Key Takeaways
- Keep controlled input values in state, and update them on every change—including when a field becomes empty.
- Use a shared change handler and a functional state update to manage the name and email fields.
- Validate separately from editing: show errors and stop invalid submission rather than rejecting keystrokes.
- Start with local state for this small form; evaluate uncontrolled inputs or a form library when your requirements change.
A small React form can keep name and email values in useState, pass them to each input through value, and update them in onChange. This React form example with useState displays those values live so you can see the connection between the inputs and component state.
React recommends function components for new code. This tutorial uses that pattern to explain local form state—not to claim that controlled inputs are a performance optimization or the right choice for every form.
ReactJS Form Example with useState
Replace App.jsx in a client-rendered React project with the following component. If you need a project first, follow the React quick start.
The example keeps the original name-and-email preview and adds a submit button, browser validation, and one custom error. Submission stays local: it doesn't call an API or save anything.
import { useRef, useState } from 'react';
export default function App() {
const [formData, setFormData] = useState({ name: '', email: '' });
const [nameError, setNameError] = useState('');
const [status, setStatus] = useState('');
const nameInput = useRef(null);
function updateState(event) {
const { name, value } = event.currentTarget;
setFormData(previous => ({ ...previous, [name]: value }));
setStatus('');
if (name === 'name') setNameError('');
}
function handleSubmit(event) {
event.preventDefault();
const error = formData.name.trim().length < 3
? 'For this demo, enter at least 3 characters.'
: '';
setNameError(error);
if (error) {
nameInput.current?.focus();
return;
}
setStatus('Demo submitted. No data was sent to a server.');
}
return (
<main>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="contact-name">Name (required)</label>
<input
ref={nameInput}
id="contact-name"
name="name"
type="text"
value={formData.name}
onChange={updateState}
required
aria-invalid={nameError ? 'true' : undefined}
aria-describedby={nameError
? 'contact-name-hint contact-name-error'
: 'contact-name-hint'}
/>
<p id="contact-name-hint">
Demo rule: at least 3 characters after trimming.
</p>
<p id="contact-name-error" role="alert">{nameError}</p>
</div>
<div>
<label htmlFor="contact-email">Email (required)</label>
<input
id="contact-email"
name="email"
type="email"
value={formData.email}
onChange={updateState}
required
/>
</div>
<button type="submit">Submit</button>
<p aria-live="polite">{status}</p>
</form>
<hr />
<table>
<caption>Live input values</caption>
<tbody>
<tr>
<th scope="row">Name</th>
<td>{formData.name}</td>
</tr>
<tr>
<th scope="row">Email</th>
<td>{formData.email}</td>
</tr>
</tbody>
</table>
</main>
);
}How controlled inputs work
A controlled input gets its displayed value from React state. Here, value={formData.name} determines the name field's text, while onChange={updateState} records each edit so React can render the new value. Without that update, React restores the previous value and the field appears uneditable.
User types
|
v
onChange reads the input value
|
v
setFormData queues a state update
|
v
React renders the updated value prop
|
v
Input and preview show the same stateuseState returns the current state and a setter. The setter requests another render; it doesn't change the state variable inside the handler that's already running. For background, review React props and state.
Updating multiple form fields
The shared handler reads each input's name and value. In this example, those names match the state keys, so [name]: value updates either name or email.
The functional updater receives the pending state. Spreading previous preserves the other field, and assigning [name] replaces only the edited field. React state objects should be replaced rather than mutated directly.
React Form Validation Without Blocking Edits
The important separation is simple: state records what the user typed; validation decides whether it can be submitted. The change handler always stores the input value, even when it's empty or temporarily invalid. This follows React's requirement to synchronize a controlled input during its change handler.
Browser constraints and custom validation
The browser handles the basic constraints: required prevents an empty field from passing validation, and type="email" checks email-address syntax. Neither confirms that an email address exists or belongs to the user.
During normal submission, a browser constraint failure prevents the submit event, so handleSubmit doesn't run yet. Once those checks pass, event.preventDefault() stops the browser's default submission and navigation, and the handler applies the custom name rule.
The demo rejects submission when the trimmed name is shorter than three characters. Editing the name clears its custom error; the rule is checked again on the next submission. The stored input itself isn't trimmed or rejected while you type.
The earlier approach—updating state only when value.length >= 3—was incorrect for these controlled fields. Starting from an empty value, it rejected the first characters; after a longer value was accepted, it also prevented ordinary deletion below the threshold.
Accessible labels and error messages
Each label's htmlFor matches its input's id, giving the field an explicit label association. Keep those IDs unique if you render multiple copies of the form.
For the custom error, aria-invalid marks the name field when validation fails, and aria-describedby connects it to the hint and error text. The error uses role="alert" to notify assistive technology of the update.
The ref has one narrow job: nameInput.current?.focus() moves focus back to the invalid name field. State still owns its value; the ref doesn't write the field's text. React documents refs for operations such as focusing an input, and W3C recommends helping users reach fields with errors.
When to Use State, FormData, or a Form Library
Choose based on what the interface needs, not on a rule that every React input must be controlled. React supports both state-controlled values and reading uncontrolled fields at submission.
| Approach | Use it when | Main trade-off |
|---|---|---|
| Controlled inputs with useState | Other UI needs the current values, as with this live preview. | You write the state updates and validation feedback. |
| Uncontrolled inputs with FormData | You mainly need values when the user submits. | Reading values at submission doesn't itself provide a live React preview. |
| A form library | Repeated validation, field interaction tracking, or shared form conventions become substantial work. | You add a dependency and learn its form-state model. |
For an uncontrolled text field, omit value and use defaultValue if it needs an initial value. Inside the form's submit handler, you can read its named controls with FormData:
// Fragment inside a form's onSubmit handler.
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get('email');The input needs name="email" for that lookup; disabled controls aren't included in the constructed form data.
For larger forms, evaluate React Hook Form's official getting-started examples, which demonstrate validation, error handling, and tracking whether fields have been edited or visited. Formik is an established alternative; check its current maintenance and integration fit rather than treating it as an automatic default for a new project.
If your main problem is complicated local update logic rather than form infrastructure, useReducer can organize state transitions in a single function. You don't need it for these two fields.
Check the Form Before Moving On
- Submit empty fields and confirm the browser blocks submission.
- Enter
Alandtest@example.com, then submit to see the custom name error. - Clear the name completely. Confirm that both the input and preview become empty instead of freezing.
- Enter
Alexand a valid email address, then submit. Confirm that the local status appears without navigation.
These are useful behaviors to automate next; see testing React components. Keep this example focused on learning the input-state relationship before adding network requests or a larger validation system.
FAQ
What is a controlled input in React?
A controlled input receives its displayed value from React state through the value prop. Its onChange handler must update that state to reflect the user's edits.
How do you handle multiple inputs with useState in React?
Store related values in an object and give each input a name matching its state key. A shared handler can use setFormData(previous => ({ ...previous, [name]: value })) to update one field while preserving the others.
How do you validate a React form without preventing users from editing?
Update the controlled value on every change, including empty values. Run validation separately on blur or submission, show an error, and block invalid submission rather than blocking state updates.
How do you handle form submission in a React function component?
Attach an onSubmit handler to the form. When handling submission in JavaScript, call event.preventDefault(), validate the values, and then perform the intended action; browser constraint failures can prevent the submit event before the handler runs.
When should you use React Hook Form instead of useState?
Evaluate React Hook Form when coordinating validation, errors, and field interaction state becomes repetitive across larger forms. Local useState remains a straightforward choice for a small form with a live preview like this one.
Further Reading
- React input reference — controlled values, labels, initial values, and common input mistakes.
- React useState reference — state setters, functional updates, and replacing objects.
- React form reference — submission handlers and reading form data.
- MDN constraint validation guide — browser rules and why server validation remains necessary.
- W3C accessible form notifications — error descriptions, announcements, and focus management.
- React DOM refs guide — accessing an input to focus it without replacing state management.
- React Hook Form getting-started documentation — complete library examples maintained in the official documentation repository.
- Formik overview — its approach to values, validation, and submission.
