React Form Handling for Beginners: Controlled Components
Learn how to handle form input in React using controlled components, with simple copy-paste examples for text fields, checkboxes, and form submission.
Learn how to handle form input in React using controlled components, with simple copy-paste examples for text fields, checkboxes, and form submission.
If you searched for how to handle form input in React, here's the short answer: React uses controlled components, where a form field's value lives in React state and updates through an onChange handler. This keeps your UI and your data in sync at all times. In this beginner-friendly guide, you'll build a working React form step by step, learn why controlled components are the standard approach, and avoid the mistakes that trip up almost every newcomer.
A controlled component is a form element (like an <input>, <textarea>, or <select>) whose current value is stored in React state instead of in the browser's own memory. Normally, when you type into an HTML input, the browser remembers what you typed. In React, we take over that job: the input's value comes from state, and every keystroke updates that state through an onChange event.
Why bother? Because once your form data lives in state, you can easily read it, validate it, send it to a server, or show it somewhere else on the page — all using normal React code, without reaching into the DOM.
Let's start with the smallest possible example: one text input connected to state.
import { useState } from "react";
function NameForm() {
const [name, setName] = useState("");
function handleChange(event) {
setName(event.target.value);
}
return (
<div>
<input type="text" value={name} onChange={handleChange} />
<p>You typed: {name}</p>
</div>
);
}
export default NameForm;Here's what each line does. useState("") creates a state variable called name, starting as an empty string, plus a function setName to update it. The value={name} prop tells the input to always show whatever is in the name state — this is what makes it 'controlled'. The onChange={handleChange} prop runs handleChange on every keystroke. Inside that function, event.target.value is the text currently in the box, and setName(...) saves it to state, which re-renders the component with the new value.
If you're new to useState, it helps to review it on its own before combining it with forms — see the linked guide at the end of this article.
Real forms usually have more than one field. Writing a separate state variable and handler for every input works, but it gets repetitive fast. A cleaner pattern is to store all fields in a single state object and use each input's name attribute to update the right key.
import { useState } from "react";
function SignupForm() {
const [form, setForm] = useState({ email: "", password: "" });
function handleChange(event) {
const { name, value } = event.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
return (
<form>
<input name="email" value={form.email} onChange={handleChange} />
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
/>
</form>
);
}form is now one object holding both email and password. Each input has a name attribute matching a key in that object. Inside handleChange, we pull out name and value from event.target, then call setForm with a function that copies the previous state (...prev) and overwrites just the field that changed ([name]: value). The square brackets around name let JavaScript use its value as the object key. This one handler now works for every input in the form, no matter how many you add.
When the user submits the form, you usually want to stop the page from reloading (the browser's default behavior) and instead run your own JavaScript, like sending the data to a server.
function handleSubmit(event) {
event.preventDefault();
console.log("Submitting:", form);
// send `form` to your API here
}
// in the JSX:
// <form onSubmit={handleSubmit}>event.preventDefault() cancels the browser's default full-page reload, which would otherwise wipe out your React app's state. After that, you're free to do whatever you need with the form data — log it, validate it, or send it with fetch() to an API endpoint.
Text inputs aren't the only form elements you'll control with state. The other common ones follow the same idea, but read slightly different properties from the event.
| Element | State reads from | Example |
|---|---|---|
| <input type="text"> | event.target.value | value={form.email} |
| <textarea> | event.target.value | value={form.message} |
| <select> | event.target.value | value={form.country} |
| <input type="checkbox"> | event.target.checked | checked={form.agree} |
Checkboxes are the one exception worth remembering: they use checked (true or false) instead of value, because a checkbox doesn't hold text — it holds a boolean on/off state.
Let's put everything together into one small, working contact form with a text input, a textarea, and a submit handler.
import { useState } from "react";
function ContactForm() {
const [form, setForm] = useState({ name: "", message: "" });
const [sent, setSent] = useState(false);
function handleChange(event) {
const { name, value } = event.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleSubmit(event) {
event.preventDefault();
setSent(true);
}
if (sent) {
return <p>Thanks, {form.name}! We got your message.</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
placeholder="Your name"
value={form.name}
onChange={handleChange}
/>
<textarea
name="message"
placeholder="Your message"
value={form.message}
onChange={handleChange}
/>
<button type="submit">Send</button>
</form>
);
}
export default ContactForm;This form tracks two pieces of state: form (the field values) and sent (whether the form was submitted). When handleSubmit runs, it prevents the page reload and flips sent to true, which swaps the form for a thank-you message. Notice both the input and the textarea use the exact same handleChange function, thanks to the name-based pattern from earlier.
A controlled component stores its value in React state and updates it through onChange, so React always knows the current value. An uncontrolled component lets the browser's DOM hold the value instead, and you read it later using a ref. Controlled components are the recommended default because they keep your data and UI in sync.
No. You can track input state without wrapping fields in a <form> tag. However, using <form> with an onSubmit handler gives you free behavior like submitting when the user presses Enter, so it's usually worth including.
Set your state object back to its initial values, for example setForm({ name: '', message: '' }), typically right after a successful submit. Because the inputs are controlled, they will immediately clear to match the new state.
Yes, as long as each input has a unique name attribute matching a key in your state object. Inside the handler, read event.target.name and event.target.value to update only the field that changed.
This happens when an input's value starts as undefined (uncontrolled) and later becomes a real string (controlled). Avoid it by always initializing state fields to an empty string, like useState(""), instead of leaving them undefined.
New to React state? Start with the fundamentals before building forms.
Read the useState guideWant a developer to build your React app or form-heavy product for you?
Talk to FepiqOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
Learn SQL CASE WHEN step by step. This beginner guide shows how to label, group and sort data with simple examples you can copy and run today.
PostgreSQL ILIKE explained for beginners: search text without worrying about case, learn % and _ wildcards, and copy simple examples you can run today.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.