In vanilla JavaScript, form inputs manage their own values. In React, you’ll often manage input values with state. This is called a controlled component — React state is the single source of truth.
value — set from state (React controls what’s displayed)
onChange — updates state when the user types
Without both, the input either won’t display the right value or won’t respond to typing.
If you set value without onChange, the input becomes read-only — the user can’t type in it. React will warn you about this in the console. Always pair value with onChange.
Checkboxes use checked and e.target.checked. Most other inputs use value and e.target.value. (File inputs are a special case and are usually handled as uncontrolled inputs.)
Even for <input type="number">, e.target.value is still a string. Convert it with Number(...) (or parseInt/parseFloat) when you actually need a number.
Always use e.preventDefault() in your submit handler. Without it, the browser reloads the page (the default HTML form behavior), which destroys your React app state. Use onSubmit on the form, not onClick on the button — this way pressing Enter also submits.
The key trick is [name]: value — a computed property name. The input’s name attribute matches the key in state, so one handleChange function works for all fields.
Every input needs a name attribute that matches the state key. handleChange reads e.target.name to know which field to update. This is the standard pattern for multi-field forms in React.
function LoginForm() { const [formData, setFormData] = useState({ email: "", password: "" }); const [errors, setErrors] = useState({}); function handleChange(e) { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // Clear error when user starts typing if (errors[name]) { setErrors(prev => ({ ...prev, [name]: null })); } } function validate() { const newErrors = {}; if (!formData.email) { newErrors.email = "Email is required"; } else if (!formData.email.includes("@")) { newErrors.email = "Invalid email address"; } if (!formData.password) { newErrors.password = "Password is required"; } else if (formData.password.length < 8) { newErrors.password = "Password must be at least 8 characters"; } return newErrors; } function handleSubmit(e) { e.preventDefault(); const newErrors = validate(); if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; // Don't submit } console.log("Valid! Submitting:", formData); // POST to API... } return ( <form onSubmit={handleSubmit}> <div> <input name="email" type="email" value={formData.email} onChange={handleChange} placeholder="Email" /> {errors.email && <p className="error">{errors.email}</p>} </div> <div> <input name="password" type="password" value={formData.password} onChange={handleChange} placeholder="Password" /> {errors.password && <p className="error">{errors.password}</p>} </div> <button type="submit">Log In</button> </form> );}
The pattern: validate on submit, show errors with &&, clear errors when the user starts fixing them.
For production apps, consider a form library like React Hook Form. But this manual pattern teaches you exactly what’s happening and works well for simple forms. Understand this pattern first, then add libraries when you need them.
Initialize state from props for edit forms. The onSave callback lets the parent component know the data was updated — this is the child-to-parent communication pattern you’ll learn in the next lesson.
You can build forms and handle user input. The last piece: how to structure your components together — which components own the data, which ones just display it, and how they communicate.