React Forms
What is a Form in React?
A form in React is a component (or group of components) that collects user input, such as text, selections, checkboxes, or file uploads and processes that data, usually by submitting it to a server or updating application state.
Definition: An interactive UI element composed of one or more input controls (text fields, dropdowns, checkboxes, etc.) whose values are managed either by React state (controlled) or the DOM itself (uncontrolled), and which handles user submission events to perform actions like API calls or page navigation.
Unlike plain HTML forms that rely on browser-native behavior, React forms integrate with the component lifecycle allowing dynamic validation, conditional rendering, and real-time feedback without page reloads.
Why Forms Are Critical in React
Capture user input for logins, registrations, surveys, and e-commerce orders.
Enforce business rules before data ever leaves the client browser.
Instant feedback, show errors as the user types, not only on submit.
Send structured payloads to REST or GraphQL endpoints on submit.
React forms don’t reload the page on submit unless you explicitly do so. TheonSubmithandler +e.preventDefault()keeps everything inside your SPA and prevents the default browser form behavior.
Controlled vs Uncontrolled Components
This is the most important conceptual split in React forms. Every input element you write falls into one of two categories:
| Feature | Controlled | Uncontrolled |
|---|---|---|
| State managed by | React state | DOM itself |
| Value binding | value={state} |
ref.current.value |
| Re-renders on change | Yes (each keystroke) | No |
| Real-time validation | Very easy | Harder |
| Best for | Most forms, real-time feedback | File inputs, simple one-off forms |
| Library support | React Hook Form, Formik | React Hook Form (register API) |
| Reset a field | setState('') |
ref.current.value = '' |
Golden Rule: Never mix controlled and uncontrolled inputs for the same field. Don’t switch between value={undefined} and value="text" ,React will warn you and behavior becomes unpredictable.
Best Practices
-
Always call e.preventDefault() first
Prevents page reload. Place it as the literal first line of every submit handler, before any async code or conditionals that might throw. -
Use a single state object for related fields
Avoid 10 separateuseStatecalls. One object + spread update is cleaner, easier to reset, and sends cleanly as a JSON payload. -
Keep validation logic pure and external
Move yourvalidate()function outside the component. It’s easier to unit test, reuse across forms, and extract into a shared utilities file. -
Show errors only after user interaction
Don’t show “required” errors before the user touches the field. Use atouchedmap or RHF’sformState.errorswhich handles this automatically. -
Disable the submit button while loading
Setdisabled={isLoading}on your submit button to prevent double submissions during async API calls. Add a spinner or loading text for feedback. -
Link labels to inputs with htmlFor + id
Always link<label htmlFor="fieldId">to<input id="fieldId">. This is an accessibility requirement — screen readers announce the label and clicking the label focuses the input. -
Reset the form after successful API submission
CallsetFormData(initialState)or RHF’sreset()after a successful API response — not just when there are no validation errors client-side. -
Never store sensitive data in URL query strings
Passwords and tokens must never appear in query strings — they end up in browser history, server logs, and analytics tools. Always use POST semantics.
FAQ’s
value makes the input controlled,React manages its value and you must provide an onChange handler. Without onChange, the input becomes read-only and React will warn you.
defaultValue sets the initial value only, then lets the DOM own the value going forward, this is the uncontrolled pattern. React never re-reads or syncs this value after mount, so state changes won’t affect what’s displayed.
This almost always means you’re defining a child component (your input or wrapper) inside the parent component’s render function. On every state change, React sees a new component type and unmounts/remounts it, wiping focus.
Fix: Always define component functions at the module level, outside the parent. Never write const MyInput = () => <input /> inside another component’s body.
Call your validate function inside handleSubmit, store the result in errors state, then return early if there are any errors:
const errs = validate(fields); setErrors(errs); if (Object.keys(errs).length > 0) return;
With React Hook Form, this is handled automatically, handleSubmit(onSubmit) only calls your onSubmit callback when all validation passes.
For controlled forms, set all state back to initial values: setFormData(initialState). Also clear your errors state: setErrors({}) and reset submitted to false.
For React Hook Form, call the reset() function returned by useForm(). You can optionally pass new default values: reset({ name: 'Rahat', email: '' }).
Use useState for: simple forms with 1–3 fields, quick prototypes, cases where form state drives other UI logic (e.g., a step wizard where step depends on form values).
Use React Hook Form for: forms with 4+ fields, complex validation, schema validation with Zod/Yup, multi-step (wizard) forms, field arrays (useFieldArray), or any production form where performance matters.
Debounce the API call in your onChange handler using setTimeout/clearTimeout, or the use-debounce library. Store the async error in state separately from sync errors.
With RHF, the validate option inside register accepts async functions: validate: async (val) => { const taken = await checkEmail(val); return taken ? 'Email already in use' : true; }
Unlike HTML where content goes between tags (<textarea>text</textarea>), React’s <textarea> uses a value prop just like an input, making it a controlled self-closing element:
<textarea value={bio} onChange={(e) => setBio(e.target.value)} rows={4} />
Track submission state with a boolean: const [isLoading, setIsLoading] = useState(false). In your submit handler, set setIsLoading(true) before the API call, and setIsLoading(false) inside a finally block.
Then: <button disabled={isLoading}>{isLoading ? 'Submitting...' : 'Submit'}</button>
A dirty field is one whose current value differs from its initial/default value. A touched field is one the user has focused and then left (blurred), regardless of whether the value changed.
Both concepts help you decide when to show validation errors. RHF tracks these automatically in formState.dirtyFields, formState.isDirty, and formState.touchedFields.
Maintain a step state (0, 1, 2…) and store all data in a shared parent state object. Conditionally render each step’s fields. Only validate the current step’s fields before advancing to the next step.
With React Hook Form, use trigger(['field1', 'field2']) to programmatically validate specific fields before proceeding. Only call handleSubmit on the final step.
Conclusion
What You Have Learned
- React Forms connect user input to component state via controlled or uncontrolled patterns, never both for the same field.
- Controlled inputs bind
valueto state andonChangeto a setter, React is the single source of truth at all times. - Multiple fields are handled with one state object + a computed property key
[e.target.name]in a shared handler. - Validation should be a pure function outside your component, triggered on submit and re-run live after the first attempt.
- Special inputs: checkboxes use
checked+e.target.checked, selects usevalueon the wrapper element, file inputs are always uncontrolled. - React Hook Form is the modern standard: zero re-renders per keystroke, built-in rules, and seamless Zod/Yup schema integration.
- Always: call
e.preventDefault()first, link labels to inputs, disable submit during loading, and reset state after success.
