All posts
ReactSeptember 14, 20268 min read

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.

F
Fepiq Team
Fepiq

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.

What Is a Controlled Component in React?

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.

Setting Up a Simple React Form

Let's start with the smallest possible example: one text input connected to state.

jsx
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.

Handling Multiple Inputs With One Change Handler

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.

jsx
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.

Handling Form Submission

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.

jsx
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.

Textareas, Checkboxes, and Selects

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.

ElementState reads fromExample
<input type="text">event.target.valuevalue={form.email}
<textarea>event.target.valuevalue={form.message}
<select>event.target.valuevalue={form.country}
<input type="checkbox">event.target.checkedchecked={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.

A Complete Example: Simple Contact Form

Let's put everything together into one small, working contact form with a text input, a textarea, and a submit handler.

jsx
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.

Common Mistakes to Avoid

  • Starting state as undefined instead of an empty string — React will warn that the input switched from uncontrolled to controlled.
  • Forgetting the name attribute when reusing one handleChange for multiple fields.
  • Forgetting event.preventDefault() in handleSubmit, which causes an unwanted page reload.
  • Using checked for text inputs or value for checkboxes — match the prop to the element type.
  • Mutating state directly (form.name = 'x') instead of calling setForm with a new object.

Frequently asked questions

What is the difference between a controlled and uncontrolled component in React?+

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.

Do I need a <form> element to handle inputs in React?+

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.

How do I reset a React form after submission?+

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.

Can I use one onChange handler for every input in a form?+

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.

Why does React show a warning about a component switching from uncontrolled to controlled?+

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 guide

Want a developer to build your React app or form-heavy product for you?

Talk to Fepiq

Get new posts in your inbox

Occasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.

Keep reading

Related posts

All posts

Let's build something

Ready to ship your next product with Fepiq?

Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.