Difficulty: Intermediate
Forms are a fundamental part of web applications, and React provides a powerful pattern for handling them called controlled components. In a controlled component, form input values are driven by React state rather than the DOM. The input's value is set by state, and every change to the input updates the state via an onChange handler. This gives React full control over the form data and enables real-time validation, dynamic formatting, and conditional form behavior.
The `onSubmit` event handler is attached to the `<form>` element and fires when the user presses Enter in an input or clicks a submit button. The first thing you should do in most onSubmit handlers is call `event.preventDefault()` - this prevents the browser's default form submission behavior, which would cause a full page reload. Instead, you handle the submission in JavaScript, typically sending the data to an API via fetch.
A controlled input follows this pattern: the input's `value` attribute is bound to a state variable, and the `onChange` handler updates that state. For example: `<input value={name} onChange={(e) => setName(e.target.value)} />`. This means React is the single source of truth for the input's value. Without the onChange handler, the input would appear frozen because the user's keystrokes would not update the state that controls the value.
For forms with multiple fields, you can either use separate useState calls for each field or a single useState with an object. The object approach uses a single handler: `const handleChange = (e) => setForm({...form, [e.target.name]: e.target.value})`. The computed property name `[e.target.name]` dynamically updates the correct field based on the input's name attribute. This scales better than individual state variables for large forms.
Input validation in controlled forms is straightforward because you have access to the current value at all times. You can validate on every keystroke (showing real-time errors), on blur (when the user leaves the field), or on submit (all at once). Controlled inputs also make it easy to implement features like character counting, auto-formatting (phone numbers, credit cards), and conditional field visibility based on other field values.
// Simulating a controlled input with state
function useState(initial) {
let state = initial;
return [() => state, (v) => { state = typeof v === 'function' ? v(state) : v; }];
}
const [getValue, setValue] = useState('');
// onChange handler updates state
function handleChange(event) {
setValue(event.target.value);
}
// Simulate user typing
handleChange({ target: { value: 'H' } });
console.log('After H: ' + getValue());
handleChange({ target: { value: 'He' } });
console.log('After He: ' + getValue());
handleChange({ target: { value: 'Hello' } });
console.log('After Hello: ' + getValue());
In a controlled input, every keystroke triggers onChange, which updates the state. The state drives the input's displayed value, creating a two-way sync.
function handleSubmit(event) {
event.preventDefault();
const formData = {
email: event.formValues.email,
password: event.formValues.password
};
// Validate
const errors = [];
if (!formData.email.includes('@')) errors.push('Invalid email');
if (formData.password.length < 6) errors.push('Password too short');
if (errors.length > 0) {
console.log('Errors: ' + errors.join(', '));
} else {
console.log('Submitting: ' + formData.email);
}
}
// Simulate valid submission
handleSubmit({
preventDefault: () => {},
formValues: { email: 'alice@test.com', password: 'secret123' }
});
// Simulate invalid submission
handleSubmit({
preventDefault: () => {},
formValues: { email: 'invalid', password: '123' }
});
Always call preventDefault() in form submit handlers to prevent page reload. Then validate the data and either show errors or submit to an API.
// Single state object for multiple form fields
let formState = {
name: '',
email: '',
role: 'user'
};
// Generic handler using computed property names
function handleFieldChange(event) {
const { name, value } = event.target;
formState = { ...formState, [name]: value };
}
// Simulate filling out the form
handleFieldChange({ target: { name: 'name', value: 'Alice' } });
handleFieldChange({ target: { name: 'email', value: 'alice@test.com' } });
handleFieldChange({ target: { name: 'role', value: 'admin' } });
console.log('Name: ' + formState.name);
console.log('Email: ' + formState.email);
console.log('Role: ' + formState.role);
Using `[event.target.name]` as a computed property key, one handler manages all form fields. Each input needs a `name` attribute matching the state key.
onSubmit, preventDefault, controlled inputs, form state, input validation