React course menu
React Forms: Controlled Inputs, Validation and Submit
Build forms in React: controlled inputs with value and onChange, text areas, selects, checkboxes and radio buttons, one handler for many fields, validation and error messages, submitting with preventDefault, and uncontrolled inputs with FormData.
On this page
Forms collect input: sign-ups, searches, checkouts, settings. The standard React approach is the controlled input: React state holds the input's value, and every keystroke updates that state.
A controlled input#
import { useState } from "react";
export default function App() {
const [city, setCity] = useState("");
return (
<div>
<input value={city} onChange={(e) => setCity(e.target.value)} placeholder="Enter a city" />
<p>{city ? `Searching flights to ${city}...` : "Type a city"}</p>
<p>{city.length} / 30 characters</p>
</div>
);
}value={city}makes the input always show the state.onChangeruns on every keystroke with the new text ine.target.value; setting state updates the input.
Because the value lives in state, you can use it anywhere: live previews, character counts, enabling or disabling buttons, validation as you type.
Other input types#
import { useState } from "react";
export default function App() {
const [message, setMessage] = useState("");
const [plan, setPlan] = useState("basic");
const [agree, setAgree] = useState(false);
const [delivery, setDelivery] = useState("standard");
return (
<div style={{ display: "grid", gap: 10, maxWidth: 360 }}>
<textarea value={message} onChange={(e) => setMessage(e.target.value)} rows={2} placeholder="Message" />
<select value={plan} onChange={(e) => setPlan(e.target.value)}>
<option value="basic">Basic — ₹199</option>
<option value="pro">Pro — ₹499</option>
</select>
<div>
{["standard", "express"].map((option) => (
<label key={option} style={{ marginRight: 12 }}>
<input
type="radio"
name="delivery"
value={option}
checked={delivery === option}
onChange={(e) => setDelivery(e.target.value)}
/>{" "}
{option}
</label>
))}
</div>
<label>
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} /> I agree to the terms
</label>
<p style={{ margin: 0 }}>
Plan: {plan}, delivery: {delivery}, agreed: {agree ? "yes" : "no"}, message length: {message.length}
</p>
</div>
);
}<textarea>and<select>usevaluein React (not inner text orselected).- Checkboxes use
checkedande.target.checked. - Radio buttons share a
name; each is checked when its value equals the state.
One state object and one handler#
For forms with many fields, keep them in one object and use the input's name to update the right field:
import { useState } from "react";
export default function App() {
const [form, setForm] = useState({ fullName: "", email: "", age: "" });
const [submitted, setSubmitted] = useState(null);
const [errors, setErrors] = useState({});
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
function validate(values) {
const found = {};
if (!values.fullName.trim()) found.fullName = "Name is required";
if (!/^\S+@\S+\.\S+$/.test(values.email)) found.email = "Enter a valid email";
if (values.age && (Number(values.age) < 13 || Number(values.age) > 120)) found.age = "Age must be 13–120";
return found;
}
function handleSubmit(e) {
e.preventDefault();
const found = validate(form);
setErrors(found);
setSubmitted(Object.keys(found).length === 0 ? form : null);
}
const field = (name, label, type = "text") => (
<label style={{ display: "block", marginBottom: 8 }}>
{label}
<br />
<input name={name} type={type} value={form[name]} onChange={handleChange} />
{errors[name] && <span style={{ color: "crimson", marginLeft: 8 }}>{errors[name]}</span>}
</label>
);
return (
<form onSubmit={handleSubmit} noValidate>
{field("fullName", "Full name")}
{field("email", "Email", "email")}
{field("age", "Age (optional)", "number")}
<button type="submit">Sign up</button>
{submitted && <p style={{ color: "green" }}>Welcome, {submitted.fullName}! We sent a link to {submitted.email}.</p>}
</form>
);
}[name]: value is a computed property name: the key comes from the variable name. Try submitting with empty or invalid fields to see the error messages.
noValidate turns off the browser's own validation popups so React's messages are shown instead. In real apps you may keep both: browser attributes such as required and type="email" are a useful first layer.
Uncontrolled inputs with FormData#
For simple forms where you only need the values on submit, you can let the browser keep the values and read them with FormData:
import { useState } from "react";
export default function App() {
const [result, setResult] = useState(null);
function handleSubmit(e) {
e.preventDefault();
setResult(Object.fromEntries(new FormData(e.currentTarget)));
}
return (
<form onSubmit={handleSubmit}>
<input name="product" defaultValue="Laptop stand" />
<input name="qty" type="number" defaultValue={1} min={1} style={{ width: 60, marginLeft: 8 }} />
<button style={{ marginLeft: 8 }}>Add to cart</button>
{result && <pre>{JSON.stringify(result)}</pre>}
</form>
);
}defaultValue sets the starting value without controlling the input. Use controlled inputs when the UI must react while typing (validation, previews, dependent fields); uncontrolled ones are fine for simple submit-only forms. Libraries such as React Hook Form build on these ideas for large forms.
Common mistakes#
Practice#
Practice exercise
Build a password field with a "Show" checkbox that toggles between hidden and visible text, and a live strength message: "Too short" under 8 characters, "OK" otherwise, and "Strong" if it is at least 12 characters and contains a digit.
Show hint
Use two state values: password and show. Set type={show ? "text" : "password"}, and test for a digit with /\d/.test(password).
Show solution
import { useState } from "react";
export default function App() {
const [password, setPassword] = useState("");
const [show, setShow] = useState(false);
let strength = "Too short";
if (password.length >= 12 && /\d/.test(password)) strength = "Strong";
else if (password.length >= 8) strength = "OK";
return (
<div>
<input type={show ? "text" : "password"} value={password} onChange={(e) => setPassword(e.target.value)} />
<label style={{ marginLeft: 8 }}>
<input type="checkbox" checked={show} onChange={(e) => setShow(e.target.checked)} /> Show
</label>
<p>{password ? strength : "Enter a password"}</p>
</div>
);
}Key takeaway#
Finished this lesson?