By WebNest Studio Engineering Team
React Tutorial
Forms and Controlled Inputs
Forms are one of the most common places where a UI needs to track live, changing data — every keystroke, checkbox toggle, or selection needs to be reflected somewhere. React offers two approaches to working with form inputs: controlled components, where React state is the single source of truth for the input's value, and uncontrolled components, where the DOM itself holds the current value and React reads it only when needed.
Controlled inputs are the more common and more "React-idiomatic" approach, because they keep form data inside your component's state, making it easy to validate, transform, or react to changes as they happen.
Why it matters
Sign-up, search, checkout and settings are all forms. In React the usual approach makes state the single source of truth for every field, which makes validation, conditional fields and submission straightforward. It is the pattern behind every form library you will use later.
Controlled Inputs: value + onChange
A controlled input's value attribute is bound to a piece of React state, and an onChange handler updates that state on every keystroke. Because the input's displayed value always comes from state rather than from whatever the user typed directly into the DOM, React is fully "in control" of what the field shows at any moment — hence the name.
Uncontrolled Inputs and refs
An uncontrolled input has no value prop tying it to state; instead, the browser DOM manages the current value internally, and you read it only when you need it — typically via a ref — such as at form submission time. Uncontrolled inputs require less code and can be useful for simple forms or when integrating with non-React code, but they make it harder to validate or react to changes as the user types.
Handling Multiple Fields and Submission
For forms with several fields, it is common to store all the field values in a single state object and update it with one shared handler that reads the input's name attribute to know which field changed. On submission, an onSubmit handler on the <form> element (paired with event.preventDefault()) is the standard place to validate and send the collected data.
- Controlled: value comes from state, onChange updates state — React state is the source of truth.
- Uncontrolled: the DOM holds the value; a ref reads it on demand, typically at submit time.
- A checkbox uses "checked" instead of "value" as its controlled prop, paired with onChange.
Example
A controlled multi-field form with a single shared change handler
import { useState } from 'react';
function SignupForm() {
const [formData, setFormData] = useState({ username: '', agreeToTerms: false });
function handleChange(event) {
const { name, value, type, checked } = event.target;
setFormData((prev) => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}));
}
function handleSubmit(event) {
event.preventDefault();
console.log('Submitting:', formData);
}
return (
<form onSubmit={handleSubmit}>
<input name="username" value={formData.username} onChange={handleChange} placeholder="Username" />
<label>
<input type="checkbox" name="agreeToTerms" checked={formData.agreeToTerms} onChange={handleChange} />
I agree to the terms
</label>
<button type="submit" disabled={!formData.agreeToTerms}>Sign Up</button>
</form>
);
}
export default SignupForm;
Typing into the username field updates formData.username live; checking the terms checkbox enables the "Sign Up" button (it stays disabled while unchecked); submitting logs the current { username, agreeToTerms } object to the console without reloading the page.
Common Mistakes
- Setting an input's value prop from state but forgetting to add an onChange handler, which makes the field appear "frozen" because React keeps resetting it to the unchanged state value.
- Mixing controlled and uncontrolled patterns on the same input, such as providing a value prop with no onChange, which triggers a React warning about a read-only field.
- Reading form values directly from the DOM with document.querySelector inside React components instead of using state or refs the React way.
- Forgetting event.preventDefault() in the form's onSubmit handler, causing an unwanted page reload on submission.
Exercise
Do this in src/App.jsx of a Vite + React project. Build a controlled form with two fields, email and password, kept in one state object and updated by a single change handler that uses the name of the input. Disable the submit button until both fields have a value.
Expected result: typing updates both fields; the Sign in button is disabled while either field is empty.
import { useState } from "react";
export default function App() {
const [form, setForm] = useState({ email: "", password: "" });
// TODO: function handleChange(event) that updates the field named event.target.name
return (
<form>
{/* TODO: email input (name, value, onChange) */}
{/* TODO: password input (name, value, onChange) */}
{/* TODO: submit button, disabled unless both fields are filled */}
</form>
);
}
Show hints
- Copy the old object and overwrite one key:
setForm({ ...form, [event.target.name]: event.target.value }). - The button takes
disabled={!form.email || !form.password}.
Show solution
import { useState } from "react";
export default function App() {
const [form, setForm] = useState({ email: "", password: "" });
function handleChange(event) {
setForm({ ...form, [event.target.name]: event.target.value });
}
return (
<form>
<input type="email" name="email" value={form.email} onChange={handleChange} />
<input type="password" name="password" value={form.password} onChange={handleChange} />
<button type="submit" disabled={!form.email || !form.password}>
Sign in
</button>
</form>
);
}
Quick quiz
1. What is a controlled input?
- A. An input that is disabled
- B. An input with a maximum length
- C. An input whose value comes from React state and is updated through onChange
- D. An input inside a form tag
Show answer
C. State is the single source of truth for what the field shows.
2. An input has a value prop but no onChange. What happens when the user types?
- A. Nothing changes; the field is effectively read-only
- B. The value updates normally
- C. The form submits
- D. React throws an error
Show answer
A. React keeps forcing the field back to the value in state and logs a warning.
3. How is the value of an uncontrolled input read?
- A. From state
- B. Through a ref to the DOM element, or from the form data on submit
- C. From props
- D. It cannot be read
Show answer
B. The DOM holds the value, and React asks for it when needed.
Interview questions
What is the difference between controlled and uncontrolled components?
Show answer
In a controlled component, the value of the field lives in React state: the input receives value and reports changes through onChange, so React always knows the current value and can validate or transform it as the user types. In an uncontrolled component, the DOM keeps the value, and the code reads it when needed through a ref or the form's data. Controlled inputs are the default; uncontrolled ones suit simple forms and file inputs.
How do you handle a form with many fields?
Show answer
Keep the fields in one state object and use a single change handler that reads the input's name and updates that key with a computed property name. Validation runs on change or on submit, with errors kept in a second object keyed by field. For large forms, a library such as React Hook Form reduces re-renders and boilerplate and integrates with schema validation.
Key Points to Remember
- Controlled inputs bind value to React state and update that state through onChange, making state the source of truth.
- Uncontrolled inputs let the DOM hold the value, read on demand via a ref.
- Checkboxes use the checked prop (not value) paired with onChange in controlled form.
- Always call event.preventDefault() in a form's onSubmit handler to stop the default page reload.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering