functionEmailInput(){const[email, setEmail]=useState('');const isValid = email.includes('@')&& email.includes('.');return(<div><input
type="email"
value={email}
onChange={e =>setEmail(e.target.value)}
className={isValid ?'valid':'invalid'}/>{!isValid && email.length>0&&(<small>Please enter a valid email address</small>)}</div>);}
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
// Traditional approachfunctionFeedbackForm(){const[message, setMessage]=useState('');const[submitted, setSubmitted]=useState(false);asyncfunctionhandleSubmit(e){
e.preventDefault();awaitsendFeedback(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.
// Validate on change, blur, or submitconst errors ={};if(!value) errors.field='Required';
Example
functionSignupForm(){const[fields, setFields]=useState({ name:'', email:'', age:''});const[errors, setErrors]=useState({});const[touched, setTouched]=useState({});functionvalidate(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;}functionhandleChange(field, value){const updated ={...fields,[field]: value };setFields(updated);if(touched[field]){setErrors(validate(updated));}}functionhandleBlur(field){setTouched(prev =>({...prev,[field]:true}));setErrors(validate(fields));}functionhandleSubmit(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">SignUp</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.
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.