Note Controlled inputs keep React as the single source of truth. The value prop locks the input to the state. If you set value without onChange, the input becomes read-only.
controlled inputinput stateform inputtext inputinput value
Note Use defaultValue (not value) for uncontrolled inputs. The DOM holds the state. Uncontrolled inputs are simpler for cases where you only need the value at submission time. For dynamic validation or conditional logic, controlled inputs are better.
uncontrolled inputdefaultValueref inputsimple formno state input
Form Submission
syntax
// Traditional
<form onSubmit={handleSubmit}>
// React 19 Actions
<form action={actionFn}>
example
// Traditional approachfunction FeedbackForm() {
const [message, setMessage] = useState('');
const [submitted, setSubmitted] = useState(false);
asyncfunction handleSubmit(e) {
e.preventDefault();
await sendFeedback(message);
setSubmitted(true);
setMessage('');
}
if (submitted) return <p>Thank you for your feedback!</p>;
return (
<form onSubmit={handleSubmit}>
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
required
/>
<button type="submit">Submit</button>
</form>
);
}
Note With onSubmit, always call e.preventDefault() to stop the browser from reloading the page. With React 19 form actions, preventDefault is handled automatically. Both patterns are valid -- actions are newer and reduce boilerplate.
form submithandle submitprevent defaultform onSubmitsend form data
Validation Patterns
syntax
// Validate on change, blur, or submitconst errors = {};
if (!value) errors.field = 'Required';
example
function SignupForm() {
const [fields, setFields] = useState({ name: '', email: '', age: '' });
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
function validate(data) {
const errs = {};
if (!data.name.trim()) errs.name = 'Name is required';
if (!data.email.includes('@')) errs.email = 'Invalid email';
if (Number(data.age) < 18) errs.age = 'Must be at least 18';
return errs;
}
function handleChange(field, value) {
const updated = { ...fields, [field]: value };
setFields(updated);
if (touched[field]) {
setErrors(validate(updated));
}
}
function handleBlur(field) {
setTouched(prev => ({ ...prev, [field]: true }));
setErrors(validate(fields));
}
function handleSubmit(e) {
e.preventDefault();
const errs = validate(fields);
setErrors(errs);
setTouched({ name: true, email: true, age: true });
if (Object.keys(errs).length === 0) {
submitSignup(fields);
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={fields.name}
onChange={e => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
/>
{touched.name && errors.name && <span>{errors.name}</span>}
{/* repeat for other fields */}
<button type="submit">Sign Up</button>
</form>
);
}
Note Validate on blur for a good UX: users see errors after leaving a field, not while typing. On submit, validate everything and mark all fields as touched. For complex forms, consider a form library or useReducer.
form validationvalidate inputerror messagesrequired fieldonBlur validation
Multiple Inputs with One Handler
syntax
function handleChange(e) {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
}
Note Use the name attribute on inputs and a computed property name ([name]) to handle many fields with one function. This scales well and avoids writing separate handlers for each field.
multiple inputssingle handlerdynamic formcomputed property namemany form fields
Note For checkboxes, read e.target.checked (boolean), not e.target.value. For selects, value goes on the <select> element, not on individual <option> elements. Radio buttons in the same group share the same name attribute.
select dropdowncheckboxradio buttonform controlschecked state