React Events and Forms: Controlled Inputs
Web DevelopmentLesson 4 of 65 min read
What you'll learn
- Attach event handlers such as onClick, onChange and onSubmit correctly
- Build controlled inputs where React state is the source of truth
- Prevent the default form submission and validate input before submitting
- Know when an uncontrolled input with FormData is the simpler choice
On this page
Forms are where most apps meet real users: sign-up, search, checkout, profile edit. React handles them in a specific way, and once you see the pattern, every form is a variation of the same few lines.
Handling events
React events are written in camelCase and take a function, not a string: onClick, onChange, onSubmit, onKeyDown. React passes your handler a synthetic event object that wraps the browser event and works the same in every browser. The most common beginner error is calling the function instead of passing it.
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
function handleClick() {
setLikes((n) => n + 1);
}
return (
<div>
{/* Correct: passes the function; React calls it on click */}
<button onClick={handleClick}>Like</button>
{/* Correct: arrow function when you need to pass an argument */}
<button onClick={() => setLikes(0)}>Reset</button>
{/* Wrong: handleClick() runs during render, not on click */}
{/* <button onClick={handleClick()}>Like</button> */}
<p>{likes} likes</p>
</div>
);
}Output
Initial: 0 likes After 2 clicks on Like: 2 likes After Reset: 0 likes
Controlled inputs: state as the source of truth
A controlled input gets its value from React state, and every keystroke fires onChange, which updates that state. The cycle is: user types, onChange runs, state updates, React re-renders the input with the new value. The browser's own copy of the text is never the authority, which means you can validate, format or reset the text with plain JavaScript.
Here is a sign-up form with a controlled email and password, a submit handler and basic validation. Note e.preventDefault(): without it the browser performs a full-page form submission and reloads the page, throwing away your state.
import { useState } from "react";
import type { SyntheticEvent } from "react";
export default function SignupForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [done, setDone] = useState(false);
function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
if (!email.includes("@")) {
setError("Enter a valid email address.");
return;
}
if (password.length < 8) {
setError("Password must be at least 8 characters.");
return;
}
setError("");
setDone(true);
// In a real app you would send the data to your API here.
}
if (done) {
return <p>Welcome, {email}! Check your inbox.</p>;
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error !== "" && <p role="alert">{error}</p>}
<button type="submit">Create account</button>
</form>
);
}Output
Submitting with email "asha" shows: Enter a valid email address. Submitting with a 5-character password: Password must be at least 8 characters. Submitting valid values replaces the form with: Welcome, asha@example.com! Check your inbox.
Many fields, one handler
A form with ten fields does not need ten useState calls. Keep one object in state, give each input a name attribute matching a key, and write a single onChange handler that uses computed property names.
import { useState } from "react";
import type { ChangeEvent } from "react";
export default function ProfileForm() {
const [form, setForm] = useState({ name: "", city: "", bio: "" });
function handleChange(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
return (
<form>
<input name="name" value={form.name} onChange={handleChange} placeholder="Name" />
<input name="city" value={form.city} onChange={handleChange} placeholder="City" />
<textarea name="bio" value={form.bio} onChange={handleChange} placeholder="Bio" />
<p>
Preview: {form.name || "Your name"} from {form.city || "somewhere"}
</p>
</form>
);
}Output
Preview: Your name from somewhere After typing "Asha" and "Pune": Preview: Asha from Pune
Uncontrolled inputs and FormData
In an uncontrolled form the DOM keeps the values and you read them only when needed, usually on submit. This is less code when you do not need live validation or live previews. Build a FormData from the form element and read fields by name.
import type { SyntheticEvent } from "react";
export default function SearchForm({ onSearch }: { onSearch: (q: string) => void }) {
function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const data = new FormData(e.currentTarget);
const query = String(data.get("query") ?? "").trim();
if (query !== "") onSearch(query);
}
return (
<form onSubmit={handleSubmit}>
<input name="query" defaultValue="" placeholder="Search jobs" />
<button type="submit">Search</button>
</form>
);
}Choose controlled when the UI must react while the user types (instant validation, character counters, disabling the submit button, formatting phone numbers). Choose uncontrolled when you only need the final values. Form libraries exist for large forms, but understanding these two approaches first makes them far easier to use.
Validation that does not annoy users
- Validate on submit first. It is simple and never shows errors before the user has had a chance to finish.
- Show the error next to the field it belongs to, and clear it when the user starts correcting it.
- Add live validation only for fields where it truly helps, such as password strength or username availability.
- Always validate again on the server. Client-side checks improve the experience but can be bypassed by anyone.
Common mistakes
- Forgetting `e.preventDefault()`. Symptom: the page flashes and reloads on submit, and your state disappears.
- Switching an input from uncontrolled to controlled. Initialising state as
undefined(useState()) and later setting a string triggers a warning about changing an uncontrolled input to controlled. Start with"". - A button without `type`. Inside a form,
<button>defaults tosubmit, so a Cancel button submits the form. Give ittype="button". - Using `onChange` for checkboxes with `e.target.value`. For checkboxes, read
e.target.checkedand bind withchecked={...}, notvalue. - Parsing numbers badly.
e.target.valueis always a string. Convert withNumber(value)and handle the empty string.
How this shows up in interviews and real projects
A very common take-home or live-coding task is a form with validation and a list that updates on submit. Interviewers watch whether you prevent the default, keep one source of truth and handle empty and error states. In real projects forms are usually the largest source of bugs, so the discipline you build here carries over. Once you can handle input, the next step is rendering collections of data in React Lists, Keys and Conditional Rendering.
A pre-ship checklist for any form
Most form bugs are not about syntax; they are about the moments between the keystroke and the server. Before you call a form finished, walk through it as a user who is impatient, offline, or both:
- Press Enter in a text field: it should submit through
onSubmit, not depend on a click handler on the button. - Double-click the submit button: the second click must be ignored while the first request is still pending.
- Submit with every field empty: each problem should be named next to the field that caused it, not in a single alert.
- Fail the request on purpose: the typed values must still be there, so nobody has to start over.
- After a successful save, decide deliberately whether to clear the fields, navigate away, or show a confirmation - and do exactly one of them.
Try it yourself
Build a job-alert form with an email input, a keyword input and a checkbox "Remote only". Validate on submit (valid email, keyword at least 2 characters) and show a confirmation line summarising the alert after a successful submit.
Show a hint
Use one state object with email, keyword and remoteOnly. For the checkbox use checked={form.remoteOnly} and read e.target.checked. Handle checkbox and text inputs in the same handler by checking e.target.type.
Key takeaways
- Pass a function to event props (
onClick={handleClick}); do not call it during render. - A controlled input binds
valueto state and updates it inonChange, so React owns the data. - Call
e.preventDefault()inonSubmitto stop the browser's full-page submission. - Use one state object and a
name-based handler to manage many fields; useFormDatawhen live updates are not needed.
Test yourself
A trick from this lesson's ride: A default submit is a trip to a new page; preventDefault cancels the trip. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is a controlled component in React?
A controlled component is a form element whose value is stored in React state. The input shows value from state and updates that state in onChange, so React is the single source of truth and can validate or transform the text on every keystroke.
Why does my React form refresh the page on submit?
The browser's default behaviour for a form submit is to send a request and reload the page. Call e.preventDefault() at the start of your onSubmit handler to stop that and handle the data in JavaScript instead.
Should I use controlled or uncontrolled inputs in React?
Use controlled inputs when the UI depends on the value as the user types, for example live validation or a disabled submit button. Use uncontrolled inputs with FormData or a ref when you only need the values at submit time, because it means less code and fewer re-renders.