Skip to main content

Command Palette

Search for a command to run...

React Forms & Controlled Components

Updated
โ€ข12 min readโ€ขView as Markdown

Almost every real-world web application needs forms.

A user might need to:

Enter their name
Enter their email
Choose a role
Enter a password
Select a preference
Submit the form

In vanilla JavaScript, we can access form elements directly from the DOM.

React takes a different approach.

Instead of letting the DOM be the primary source of truth, we can store form values in React State.

This pattern is called a Controlled Component.


๐Ÿง  What Is a Form in React?

A normal HTML form might look like:

<form>
  <input type="text" />
  <button type="submit">Submit</button>
</form>

In React, we can connect the input to state:

const [name, setName] = useState("");

Then:

<input
  value={name}
  onChange={event => setName(event.target.value)}
/>

Now React knows the current value of the input.

The flow becomes:

User types
    โ†“
onChange
    โ†“
event.target.value
    โ†“
setName()
    โ†“
State updates
    โ†“
Input re-renders

โš›๏ธ What Is a Controlled Component?

A controlled input is an input whose value is controlled by React state.

Example:

import { useState } from "react";

function App() {
  const [name, setName] = useState("");

  return (
    <input
      value={name}
      onChange={event => setName(event.target.value)}
    />
  );
}

Here:

value={name}

means React controls the input's value.

And:

onChange={event => setName(event.target.value)}

updates that state whenever the user types.

So:

React State
     โ†“
   Input
     โ†“
User types
     โ†“
onChange
     โ†“
React State

This is called controlled input or controlled component.


๐Ÿ”„ Why Controlled Components Matter

At first, you might think:

"Why not just read the input when the user submits?"

Because controlling inputs gives React access to the current value at all times.

That means we can easily:

  • Validate input

  • Disable buttons

  • Display live previews

  • Show character counts

  • Enable/disable fields

  • Transform values

  • Display error messages

  • Submit data to APIs

For example:

<p>{name.length}/50 characters</p>

Because React already knows the current value.


๐Ÿ“ Basic Text Input

Let's start with a simple name input.

import { useState } from "react";

function App() {
  const [name, setName] = useState("");

  function handleNameChange(event) {
    setName(event.target.value);
  }

  return (
    <div>
      <label>Name:</label>

      <input
        type="text"
        value={name}
        onChange={handleNameChange}
      />

      <p>Your name: {name}</p>
    </div>
  );
}

export default App;

If the user types:

Saurabh

the state becomes:

"Saurabh"

and the UI updates immediately.


๐ŸŽฏ event.target.value

This is one of the most important things to remember.

For:

<input onChange={handleChange} />

we can write:

function handleChange(event) {
  console.log(event.target.value);
}

If the user types:

React

then:

event.target.value

contains:

"React"

This is how we retrieve what the user entered.


๐Ÿ“ง Handling Multiple Inputs

Real forms usually have multiple fields.

For example:

Name
Email
Password

We could create separate state variables:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

Then:

<input
  value={name}
  onChange={event => setName(event.target.value)}
/>

<input
  value={email}
  onChange={event => setEmail(event.target.value)}
/>

<input
  value={password}
  onChange={event => setPassword(event.target.value)}
/>

This works perfectly well.

But for larger forms, there's another useful pattern.


๐Ÿ“ฆ Managing Form Data With One Object

Instead of:

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

we can use:

const [formData, setFormData] = useState({
  name: "",
  email: "",
  password: ""
});

Now all form data lives inside one object.


๐Ÿ”„ Updating Object State

Suppose the user changes the name.

We can write:

setFormData({
  ...formData,
  name: event.target.value
});

The spread operator keeps the existing fields.

For example:

{
  name: "Saurabh",
  email: "",
  password: ""
}

If the email changes:

setFormData({
  ...formData,
  email: event.target.value
});

The other values remain unchanged.


๐Ÿง  Dynamic Input Handling With name

We can make this much cleaner.

Give each input a name:

<input
  name="name"
  value={formData.name}
  onChange={handleChange}
/>

<input
  name="email"
  value={formData.email}
  onChange={handleChange}
/>

Then create one handler:

function handleChange(event) {
  const { name, value } = event.target;

  setFormData({
    ...formData,
    [name]: value
  });
}

This is a very useful React form pattern.


๐Ÿ” Understanding [name]: value

This syntax:

[name]: value

is a computed property name in JavaScript.

Suppose:

name = "email";
value = "test@example.com";

Then:

{
  [name]: value
}

becomes:

{
  email: "test@example.com"
}

If:

name = "username";

it becomes:

{
  username: "..."
}

This allows one event handler to manage multiple fields.


๐Ÿš€ Complete Multi-Input Form

import { useState } from "react";

function RegistrationForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    password: ""
  });

  function handleChange(event) {
    const { name, value } = event.target;

    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  }

  function handleSubmit(event) {
    event.preventDefault();

    console.log(formData);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />

      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />

      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />

      <button type="submit">
        Register
      </button>
    </form>
  );
}

export default RegistrationForm;

Notice the pattern:

Input
 โ†“
name + value
 โ†“
handleChange
 โ†“
setFormData()
 โ†“
State

This pattern is worth memorizing.


๐Ÿ“ค Handling Form Submission

Forms use:

onSubmit

Example:

<form onSubmit={handleSubmit}>

Handler:

function handleSubmit(event) {
  event.preventDefault();

  console.log("Submitted!");
}

The:

event.preventDefault();

prevents the browser's default form submission behavior.

Now React can handle the submission.


๐Ÿ›‘ Why Use preventDefault()?

Without it, the browser may perform its normal form submission behavior.

In a React single-page application, we usually want to:

Collect data
    โ†“
Validate data
    โ†“
Send API request
    โ†“
Show result

instead of allowing the browser to navigate/reload as part of the default submission.


๐Ÿ” Password Input

Password fields work exactly like normal controlled inputs:

const [password, setPassword] = useState("");

Then:

<input
  type="password"
  value={password}
  onChange={event => setPassword(event.target.value)}
/>

We can later combine this with conditional rendering to create:

Password: ********

[ Show Password ]

๐Ÿ“‹ Textarea

A <textarea> can also be controlled.

const [message, setMessage] = useState("");

Then:

<textarea
  value={message}
  onChange={event => setMessage(event.target.value)}
/>

Example:

function MessageForm() {
  const [message, setMessage] = useState("");

  return (
    <div>
      <textarea
        value={message}
        onChange={event => setMessage(event.target.value)}
        placeholder="Write a message..."
      />

      <p>{message.length} characters</p>
    </div>
  );
}

๐Ÿ”˜ Checkbox

Checkboxes are slightly different.

Instead of:

value

we generally control:

checked

Example:

const [accepted, setAccepted] = useState(false);

Then:

<input
  type="checkbox"
  checked={accepted}
  onChange={event => setAccepted(event.target.checked)}
/>

Notice:

event.target.checked

rather than:

event.target.value

๐Ÿง  Why checked?

A checkbox has a boolean state:

true
false

So:

checked={accepted}

makes more semantic sense.

Example:

<label>
  <input
    type="checkbox"
    checked={accepted}
    onChange={event => setAccepted(event.target.checked)}
  />

  I accept the terms
</label>

Now accepted will be either:

true

or:

false

๐Ÿ”˜ Radio Buttons

Radio buttons work similarly.

Suppose we want users to select a role:

โ—‹ Frontend Developer
โ—‹ Backend Developer
โ—‹ Full Stack Developer

State:

const [role, setRole] = useState("");

Then:

<label>
  <input
    type="radio"
    name="role"
    value="frontend"
    checked={role === "frontend"}
    onChange={event => setRole(event.target.value)}
  />

  Frontend Developer
</label>

Another:

<label>
  <input
    type="radio"
    name="role"
    value="backend"
    checked={role === "backend"}
    onChange={event => setRole(event.target.value)}
  />

  Backend Developer
</label>

The state stores the selected value.


๐Ÿ”ฝ Select Dropdown

A <select> can also be controlled.

const [country, setCountry] = useState("");

Then:

<select
  value={country}
  onChange={event => setCountry(event.target.value)}
>
  <option value="">Select Country</option>
  <option value="india">India</option>
  <option value="usa">USA</option>
  <option value="uk">UK</option>
</select>

Now:

User selects India
       โ†“
onChange
       โ†“
event.target.value
       โ†“
setCountry("india")

๐Ÿงฉ Form Validation

Forms shouldn't blindly accept anything.

We can validate data before submitting.

For example:

function handleSubmit(event) {
  event.preventDefault();

  if (!formData.name) {
    alert("Name is required");
    return;
  }

  if (!formData.email) {
    alert("Email is required");
    return;
  }

  console.log("Form submitted!");
}

The logic becomes:

Submit
  โ†“
Validate
  โ†“
Invalid?
 โ”œโ”€โ”€ Yes โ†’ Show error
 โ””โ”€โ”€ No  โ†’ Submit

โŒ Displaying Validation Errors

Instead of using alert(), we can store errors in state.

const [error, setError] = useState("");

Then:

if (!formData.name) {
  setError("Name is required");
  return;
}

Display:

{error && <p>{error}</p>}

This uses the conditional rendering concept we learned on Day 65.


๐Ÿ”ฅ Multiple Validation Errors

For larger forms, we can store errors as an object:

const [errors, setErrors] = useState({});

Example:

setErrors({
  name: "Name is required",
  email: "Invalid email"
});

Then:

{errors.name && (
  <p>{errors.name}</p>
)}

and:

{errors.email && (
  <p>{errors.email}</p>
)}

This gives us much more control.


๐Ÿง  Validation + Conditional Rendering

Suppose:

{errors.email && (
  <span>{errors.email}</span>
)}

If:

errors.email

contains:

"Invalid email"

we show the message.

If it doesn't exist:

undefined

nothing is displayed.

This is another example of combining concepts from previous days.


๐Ÿ”ข Input Validation Example

We can validate an email:

function validateEmail(email) {
  return email.includes("@");
}

Then:

if (!validateEmail(formData.email)) {
  setErrors({
    email: "Please enter a valid email"
  });

  return;
}

For production applications, validation requirements are usually more comprehensive, but this demonstrates the basic idea.


๐Ÿšซ Disabling Submit Buttons

We can prevent submission until required fields are filled.

For example:

<button
  type="submit"
  disabled={!formData.email || !formData.password}
>
  Login
</button>

If either field is empty, the button is disabled.

This is a great example of:

State
 โ†“
Condition
 โ†“
UI behavior

๐Ÿ”„ Resetting a Form

After successful submission, we may want to clear the form.

setFormData({
  name: "",
  email: "",
  password: ""
});

For example:

function handleSubmit(event) {
  event.preventDefault();

  console.log(formData);

  setFormData({
    name: "",
    email: "",
    password: ""
  });
}

Now the inputs return to their initial values.


๐Ÿง  Controlled vs Uncontrolled Components

There are two broad approaches.

Controlled

React state controls the value:

<input
  value={name}
  onChange={handleChange}
/>

Uncontrolled

The DOM keeps track of the value, often accessed through a ref.

For example:

<input ref={inputRef} />

For many everyday React forms, controlled components are a straightforward and predictable approach.

We'll learn more about refs later.


๐Ÿ“Š Controlled Component Mental Model

Remember this:

         React State
              โ†“
           value
              โ†“
            Input
              โ†“
         User types
              โ†“
          onChange
              โ†“
        setState(...)
              โ†“
         React State

React stays in control of the data.


๐Ÿ”ฅ Complete Registration Form

Let's put everything together.

import { useState } from "react";

function RegistrationForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    password: "",
    role: ""
  });

  const [error, setError] = useState("");

  function handleChange(event) {
    const { name, value } = event.target;

    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  }

  function handleSubmit(event) {
    event.preventDefault();

    if (!formData.name) {
      setError("Name is required");
      return;
    }

    if (!formData.email) {
      setError("Email is required");
      return;
    }

    if (!formData.password) {
      setError("Password is required");
      return;
    }

    setError("");

    console.log("Registration successful!");
    console.log(formData);
  }

  return (
    <form onSubmit={handleSubmit}>
      <h1>Create Account</h1>

      {error && (
        <p>{error}</p>
      )}

      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />

      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />

      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />

      <select
        name="role"
        value={formData.role}
        onChange={handleChange}
      >
        <option value="">Select Role</option>
        <option value="frontend">
          Frontend Developer
        </option>
        <option value="backend">
          Backend Developer
        </option>
        <option value="fullstack">
          Full Stack Developer
        </option>
      </select>

      <button type="submit">
        Register
      </button>
    </form>
  );
}

export default RegistrationForm;

This single example uses:

  • useState

  • Controlled inputs

  • onChange

  • onSubmit

  • event.target

  • Computed property names

  • Object state

  • Conditional rendering

  • Validation

  • Select inputs

  • Error handling

  • Form reset concepts


๐Ÿ† Best Practices

1. Give inputs meaningful name attributes

name="email"

This makes generic handlers much easier.


2. Keep form state predictable

For example:

const [formData, setFormData] = useState({
  name: "",
  email: "",
  password: ""
});

3. Use functional state updates for object state

Prefer:

setFormData(prev => ({
  ...prev,
  [name]: value
}));

This clearly updates based on the previous state.


4. Validate before submitting

Don't assume the user entered valid data.


5. Give useful error messages

Instead of:

Error

prefer:

Please enter a valid email address.

6. Keep validation logic separate when it gets complex

As applications grow, move validation into dedicated functions or utilities.


7. Don't store unnecessary derived state

For example, if you can calculate:

const isFormValid =
  formData.email !== "" &&
  formData.password !== "";

you don't necessarily need another state variable just for isFormValid.


๐Ÿ’ก My Biggest Takeaway

Today's biggest takeaway is:

A controlled form is essentially a conversation between React State and the user.

The user types:

User Input
    โ†“
onChange
    โ†“
State
    โ†“
UI

And when the user submits:

Submit
 โ†“
onSubmit
 โ†“
Validate
 โ†“
Process Data
 โ†“
API / Backend

This is extremely important because forms are the primary way users provide data to our applications.

And soon, when we connect React to a backend, this same form data can be sent to our Node.js / Express APIs.

100 Days of Code: My Journey to Becoming a Full Stack Developer

Part 1 of 50

Welcome to my 100 Days of Code journey! In this series, I'll document my daily progress as I learn Full Stack Web Development from the ground up. Every post will cover what I learned, challenges I faced, mistakes I made, and the projects I built. My goal is not just to complete 100 days but to become a better developer through consistency, discipline, and learning in public. Topics I'll cover include: โ€ข Git & GitHub โ€ข HTML, CSS & JavaScript โ€ข React.js โ€ข Node.js & Express โ€ข MongoDB โ€ข APIs โ€ข Real-world Projects โ€ข AI tools for Developers Whether you're just starting out or revising your fundamentals, I hope this journey helps you learn alongside me. Let's build, learn, and grow together! ๐Ÿš€

More from this blog

T

TheSaurceCode

73 posts

Documenting my journey to becoming a Full Stack Developer through daily blogs, coding challenges, projects, tutorials, and lessons learned. Learn, build, and grow with me