1. đ Introduction
Forms are the backbone of nearly every interactive web application â login screens, checkout flows, search bars, and settings pages all rely on them. In React, forms behave a little differently than in plain HTML because React encourages you to keep the UI in sync with your application state. This tutorial walks through everything from the basics of <input> elements to advanced validation, accessibility, performance, and popular form libraries like React Hook Form and Formik.
Information
2. â What are Forms?
A form is a collection of interactive elements â input, textarea, select, button, and more â that allow users to submit data to an application. In traditional HTML, forms submit data to a server and trigger a full page reload. In React, we usually intercept that submission and handle it entirely with JavaScript, giving us full control over validation, feedback, and data flow.
- Collecting user input (text, numbers, files, etc.)
- Validating that input before it's used
- Submitting data to an API or parent component
- Providing feedback like errors, loading, and success states
3. đ§ą React Form Basics
Unlike plain HTML, where form elements manage their own internal state, React typically manages form data through state. This lets you read, validate, and manipulate values at any time, rather than only when the form is submitted.
BasicForm.jsx
function BasicForm() {
const [name, setName] = useState("");
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</form>
);
}Here, the <input>'s value is driven entirely by React state â a pattern known as a controlled component, which we'll explore next.
4. đī¸ Controlled Components
A controlled component is a form element whose value is set and updated by React state, rather than by the DOM itself. Every keystroke triggers an onChange handler, which updates state, which then re-renders the input with the new value.
ControlledInput.jsx
function ControlledInput() {
const [email, setEmail] = useState("");
const handleChange = (e) => setEmail(e.target.value);
return (
<input
type="email"
value={email}
onChange={handleChange}
placeholder="you@example.com"
/>
);
}Best Practice
5. đšī¸ Uncontrolled Components
An uncontrolled component lets the DOM manage its own state internally. Instead of tracking every change with onChange, you read the value only when you need it â typically using a ref.
UncontrolledInput.jsx
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert(inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} defaultValue="Hello" />
<button type="submit">Submit</button>
</form>
);
}Notice the use of defaultValue instead of value â this tells React "set this once, then leave it alone."
6. âī¸ Controlled vs Uncontrolled Components
Choosing between the two approaches depends on your form's complexity, performance needs, and how much control you want over each keystroke.
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | DOM |
| Real-time validation | Easy | Harder |
| Performance on large forms | Can re-render often | Fewer re-renders |
| Integration with 3rd-party DOM libs | Trickier | Easier |
| Typical use case | Most forms | File inputs, simple one-off forms |
Tip
7. đī¸ Form State Management
As forms grow beyond a couple of fields, tracking each one with a separate useState call becomes unwieldy. A common solution is to consolidate all field values into a single state object.
FormState.jsx
function SignupForm() {
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
return (
<form>
<input name="username" value={formData.username} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<input name="password" type="password" value={formData.password} onChange={handleChange} />
</form>
);
}Note
8. â¨ī¸ Handling User Input
Every form input generates an onChange event containing an event.target object. This object exposes the field's value (and, for checkboxes, its checked state) so you can update your state accordingly.
9. đ¤ Text Input
The most common form element is the plain text <input>. It supports many type values including text, email, password, number, tel, and url.
TextInput.jsx
<input
type="text"
name="fullName"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
placeholder="Full name"
/>10. đ Textarea
Unlike plain HTML, where <textarea> content sits between the tags, React's <textarea> uses a value attribute â just like <input>.
Textarea.jsx
<textarea
name="bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
rows={4}
/>11. đŊ Select Dropdown
React's <select> also uses value on the parent element rather than an selected attribute on individual <option> elements.
SelectDropdown.jsx
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="in">India</option>
<option value="de">Germany</option>
</select>12. âī¸ Checkbox
Checkboxes are boolean inputs, so they use checked instead of value.
Checkbox.jsx
<label>
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
I agree to the <b>terms and conditions</b>
</label>13. đ Radio Buttons
Radio buttons share a common name so only one option in the group can be selected at a time. In React, you compare each radio's value against the current state to determine checked.
RadioButtons.jsx
function PlanPicker() {
const [plan, setPlan] = useState("basic");
return (
<>
{["basic", "pro", "enterprise"].map((option) => (
<label key={option}>
<input
type="radio"
name="plan"
value={option}
checked={plan === option}
onChange={(e) => setPlan(e.target.value)}
/>
{option}
</label>
))}
</>
);
}14. đ File Input
File inputs are always uncontrolled in React â you cannot programmatically set a file's value for security reasons. Instead, read the selected files from event.target.files.
FileInput.jsx
function AvatarUpload() {
const [file, setFile] = useState(null);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
return (
<input type="file" accept="image/*" onChange={handleFileChange} />
);
}Warning
15. đ§Š Multiple Inputs
When a form has many fields, a shared handleChange function keyed off event.target.name keeps your code DRY.
MultipleInputs.jsx
function ProfileForm() {
const [values, setValues] = useState({ firstName: "", lastName: "", age: "" });
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setValues((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
return (
<form>
<input name="firstName" value={values.firstName} onChange={handleChange} />
<input name="lastName" value={values.lastName} onChange={handleChange} />
<input name="age" type="number" value={values.age} onChange={handleChange} />
</form>
);
}16. â Dynamic Forms
Sometimes a form needs a variable number of fields â for example, adding multiple phone numbers or line items to an invoice. Store these as an array in state, and render one input per array item.
DynamicForm.jsx
function PhoneList() {
const [phones, setPhones] = useState([""]);
const updatePhone = (index, value) => {
const updated = [...phones];
updated[index] = value;
setPhones(updated);
};
const addPhone = () => setPhones([...phones, ""]);
const removePhone = (index) => setPhones(phones.filter((_, i) => i !== index));
return (
<>
{phones.map((phone, i) => (
<div key={i}>
<input value={phone} onChange={(e) => updatePhone(i, e.target.value)} />
<button type="button" onClick={() => removePhone(i)}>Remove</button>
</div>
))}
<button type="button" onClick={addPhone}>Add phone</button>
</>
);
}Important
17. đ¤ Form Submission
The <form> element's onSubmit handler is where you typically validate data and send it onward â to an API, a parent component, or a state manager.
SubmitForm.jsx
function ContactForm() {
const [message, setMessage] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log("Submitting:", message);
};
return (
<form onSubmit={handleSubmit}>
<textarea value={message} onChange={(e) => setMessage(e.target.value)} />
<button type="submit">Send</button>
</form>
);
}18. đ Preventing Default Submission
By default, submitting an HTML form triggers a full page reload and navigates to the form's action URL. Calling event.preventDefault() inside your onSubmit handler stops this, letting React handle the submission entirely in JS.
Caution
19. đĨ Reading Form Values
There are two main strategies for reading form values at submission time.
FromState.jsx
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData); // already in state
};FromFormData.jsx
const handleSubmit = (e) => {
e.preventDefault();
const data = new FormData(e.target);
console.log(Object.fromEntries(data));
};The FormData approach works well for uncontrolled forms since it reads directly from the DOM, without needing state at all.
20. đ Resetting Forms
For controlled forms, reset by setting state back to its initial values. For uncontrolled forms, you can call the native form.reset() method or change the input's key to force React to remount it.
ResetForm.jsx
const initialState = { name: "", email: "" };
const [formData, setFormData] = useState(initialState);
const handleReset = () => setFormData(initialState);21. â Form Validation
Validation ensures the data a user submits is correct, complete, and safe to process. React supports both the browser's built-in validation and fully custom, JS-driven validation.
22. đ Native HTML Validation
HTML attributes like required, minLength, maxLength, pattern, and type="email" trigger the browser's built-in validation UI automatically, with no extra code.
NativeValidation.jsx
<input type="email" required minLength={5} placeholder="Email" />Tip
23. đ ī¸ Custom Validation
Custom validation gives you full control over rules, timing, and error messages by writing your own validation functions in JS.
CustomValidation.jsx
function validate(values) {
const errors = {};
if (!values.email.includes("@")) errors.email = "Enter a valid email";
if (values.password.length < 8) errors.password = "Minimum 8 characters";
return errors;
}24. đ¨ Error Handling
Store validation errors in their own piece of state, then conditionally render error messages next to each field.
ErrorHandling.jsx
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate(formData);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
// proceed with submission
}
};25. âąī¸ Real-Time Validation
Running validation inside onChange or onBlur gives users immediate feedback as they type or move between fields, rather than waiting until submission.
RealTimeValidation.jsx
const handleBlur = (e) => {
const { name, value } = e.target;
const fieldErrors = validate({ ...formData, [name]: value });
setErrors((prev) => ({ ...prev, [name]: fieldErrors[name] }));
};Best Practice
26. âŗ Async Validation
Some checks â like confirming a username is unique â require a server round-trip. Debounce these calls so you're not hitting the server on every keystroke.
AsyncValidation.jsx
useEffect(() => {
if (!username) return;
const timeout = setTimeout(async () => {
const res = await fetch(`/api/check-username?u=${username}`);
const { available } = await res.json();
setErrors((prev) => ({ ...prev, username: available ? "" : "Username taken" }));
}, 500);
return () => clearTimeout(timeout);
}, [username]);27. đĢ Disabled States
Disable the submit button while a form is invalid or mid-submission to prevent duplicate or premature submissions.
DisabledState.jsx
<button type="submit" disabled={isSubmitting || Object.keys(errors).length > 0}>
Submit
</button>28. âŗ Loading States
Track an isSubmitting flag to show spinners, disable inputs, and prevent duplicate network requests while an async submission is in flight.
LoadingState.jsx
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
await submitForm(formData);
} finally {
setIsSubmitting(false);
}
};29. âŋ Form Accessibility
Accessible forms ensure every user â including those using screen readers or keyboard-only navigation â can understand and complete them.
- Always pair inputs with a <label> using htmlFor/id
- Use aria-invalid and aria-describedby to associate error messages with fields
- Ensure focus order follows a logical, visual sequence
- Announce errors with role="alert" so screen readers pick them up automatically
Important
30. ⥠Form Performance
Large forms with dozens of controlled inputs can suffer from unnecessary re-renders. A few techniques help keep things fast:
- Split large forms into smaller, memoized subcomponents
- Use uncontrolled inputs with refs for fields that don't need real-time validation
- Debounce expensive validation or API calls
- Consider a form library (see Section 31) that avoids re-rendering the whole tree per keystroke
31. đĻ Form Libraries Overview
As forms grow in complexity â nested fields, arrays, cross-field validation, async checks â hand-rolled state management becomes harder to maintain. Dedicated libraries solve these problems out of the box.
| Library | Approach | Best for |
|---|---|---|
| React Hook Form | Uncontrolled + refs | Performance-sensitive, large forms |
| Formik | Controlled + render props/hooks | Familiar, batteries-included API |
32. đĒ React Hook Form
React Hook Form uses uncontrolled inputs under the hood via refs, meaning most fields don't cause re-renders on every keystroke â making it very fast for large forms.
ReactHookForm.jsx
import { useForm } from "react-hook-form";
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email", { required: true })} />
{errors.email && <span>Email is required</span>}
<button type="submit">Login</button>
</form>
);
}Learn more in the official React Hook Form docs.
33. đ§Ē Formik
Formik takes a controlled, hooks-and-components approach, bundling state management, validation (often paired with Yup), and submission handling into one cohesive API.
FormikExample.jsx
import { Formik, Form, Field, ErrorMessage } from "formik";
function SignupForm() {
return (
<Formik
initialValues={{ email: "" }}
validate={(values) => {
const errors = {};
if (!values.email) errors.email = "Required";
return errors;
}}
onSubmit={(values) => console.log(values)}
>
<Form>
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" />
<button type="submit">Sign up</button>
</Form>
</Formik>
);
}See the Formik documentation for the full API.
34. đˇ TypeScript with Forms
Typing form state clarifies exactly what shape your data takes and catches typos in field names at compile time rather than at runtime.
TypedForm.tsx
interface SignupValues {
username: string;
email: string;
age: number;
}
function SignupForm() {
const [values, setValues] = useState<SignupValues>({
username: "",
email: "",
age: 0,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues((prev) => ({ ...prev, [name]: value }));
};
return <form>{/* fields here */}</form>;
}35. đ Best Practices
- Prefer controlled components unless you have a specific performance reason not to
- Consolidate related fields into a single state object rather than many useState calls
- Always call event.preventDefault() in your submit handler
- Validate both on the client and the server â never trust client-side validation alone
- Give every input a matching <label> for accessibility
- Use a form library once your form exceeds a handful of fields or needs complex validation
36. â ī¸ Common Mistakes
- Forgetting event.preventDefault(), causing an unwanted page reload
- Mixing controlled and uncontrolled patterns on the same input (e.g. providing both value and no onChange)
- Using array indices as key in dynamic forms, causing state to "stick" to the wrong row
- Not resetting error state after a successful submission
- Skipping labels and relying only on placeholder text for accessibility
Danger
37. đŦ Frequently Asked Questions
Why does React warn about a "controlled input changing to uncontrolled"?
This happens when an input's value switches from a defined value (like an empty string) to undefined or null â often because a state field was never initialized. Always initialize controlled fields to an empty string, not undefined.
Should I validate on every keystroke?
Generally, no â validating on onBlur or on submit feels friendlier. Real-time, per-keystroke validation is best reserved for things like password strength meters where instant feedback adds real value.
Do I need a form library for a simple contact form?
Usually not. Plain useState and a couple of handlers are enough for small forms. Reach for a library once you're managing complex validation, nested fields, or many interdependent inputs.