Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 28

Controlled Components

Recommended resources

Hello! Welcome to the first lesson in our module on working with forms in React.

In the last lesson, we built a dynamic search filter. To do that, we used an <input> element and connected its value to a state variable using the value and onChange props. We were, in fact, already using the primary pattern for handling forms in React without giving it a formal name.

Today, we're going to dive deep into that pattern. You'll learn about controlled components, which are the cornerstone of building predictable and maintainable forms in React.

By the end of this lesson, you will be able to: create controlled form inputs by binding their value to state. This is a fundamental skill that you'll use in almost every React application you build.


What are Controlled Components?

In React, we have two main approaches for handling form inputs: uncontrolled and controlled.

  • Uncontrolled Components: In this approach, the DOM is in charge. The input element itself keeps track of its own value. To get the value in React, you have to "pull" it from the DOM when you need it (e.g., when a form is submitted).
  • Controlled Components: In this approach, React is in charge. The value of the input element is driven by the component's state. The React state is the "single source of truth".

This diagram provides a great visual summary of the data flow in both approaches.

Controlled vs. Uncontrolled Components Data Flow
This diagram illustrates the key difference in data flow. In a controlled component (left), the parent component's state dictates the input's value, and user actions update that state. In an uncontrolled component (right), the input manages its own internal state, and the parent must query the DOM directly to get its value.

For building robust and interactive forms, the controlled component pattern is the standard in React. It allows your component's state to manage the form's data, which makes it much easier to perform validation, respond to changes in real-time, or reset form fields.


The Two-Way Data Binding Loop

So, how do we make an input "controlled" by React? It's a two-step process that creates a loop, often called two-way data binding.

To understand this concept clearly, let's start with an excellent article from Josh Comeau's blog.

Data binding in React: how to work with forms in React - Josh Comeau

This article, 'Data binding in React', provides one of the clearest explanations of how controlled components work by breaking down the concept into 'one-way' and 'two-way' data binding.

Please read the section 'Introduction to controlled fields'. Focus on understanding the difference between a locked input (one-way binding) and a fully functional input (two-way binding), and note the two key ingredients required.

As the article explains, the two essential ingredients for a controlled input are:

  1. The value prop: You pass a state variable to the input's value prop. This forces the input's displayed value to always match your React state. (State → UI)
  2. The onChange handler: You provide a function that updates your state variable whenever the user tries to edit the input. This function receives an event object, and the new value is typically found at event.target.value. (UI → State)

When you combine these two, you create a complete loop. The state drives the input's value, and the input's changes drive the state.



Implementing a Controlled Input

Now let's see this in practice. The official React documentation provides the most direct and authoritative guide on this topic.

<input> – React

The React documentation gives a concise, code-focused explanation of how to control an input with a state variable.

Please read the sections: 'Controlling an input with a state variable': Pay close attention to the code example. Notice how useState, value, and onChange all work together. 'My text input doesn’t update when I type into it': This troubleshooting section explains the critical 'pitfall' of what happens if you provide a value prop without an onChange handler. This is a very common beginner mistake.

Let's break down the code from the documentation into three simple steps:

1. Declare a state variable:
Initialize it with an empty string. Using '' instead of null or undefined prevents React from throwing an error about switching from an uncontrolled to a controlled input.

import { useState } from 'react';

function MyForm() {
  const [name, setName] = useState(''); // Step 1
  // ...
}

2. Bind the value and onChange props:
In your JSX, connect the state variable and its setter function to the <input> element.

return (
  <input
    value={name} // Step 2a: Bind the value to the 'name' state
    onChange={e => setName(e.target.value)} // Step 2b: Update state on change
  />
);

That's it! You now have a fully controlled text input.


Applying the Pattern to Other Form Elements

This powerful pattern isn't just for text inputs. It works consistently across other common form elements like <textarea> and <select>.

Let's watch a short video that demonstrates how to apply this exact same logic to different elements.

Full React Tutorial #27 - Controlled Inputs (forms)

The Net Ninja provides a quick and practical demonstration of creating controlled inputs for a text field, a textarea, and a select dropdown.

Watch the video from the beginning to 08:51. As you watch, notice how the core pattern (useState, value, onChange) is repeated for each element, even though the elements themselves are different: Intro (00:13 - 00:46): A quick explanation of controlled inputs. Text Input (04:08 - 06:32): Shows the full implementation for a standard text input. Textarea (07:00 - 07:34): Demonstrates that the implementation is identical for a <textarea>. Select (07:48 - 08:51): Shows how to control a <select> dropdown. Notice that the value prop is on the <select> tag itself, not the <option> tags, which is a convenience provided by React.

The key takeaway from the video is the consistency of the pattern. React smooths over the differences between these HTML elements, giving you a single, unified way to manage their data.



Practice Exercise

Let's put this into practice. Your task is to build a simple profile editor form.

Goal: Create a component with two controlled inputs:

  1. A text input for a username.
  2. A <select> dropdown for a userRole with three options: "Guest", "Editor", and "Admin".

Below the form, display the current values of username and userRole to see them update in real-time.

Starter Code:

import { useState } from 'react';

function ProfileEditor() {
  // 1. Create state for 'username' and 'userRole' here.
  //    Initialize 'username' to '' and 'userRole' to 'Guest'.

  return (
    <form>
      <div>
        <label>
          Username:
          {/* 2. Add a controlled text input here. */}
          {/*    It should be connected to the 'username' state. */}
        </label>
      </div>

      <div>
        <label>
          Role:
          {/* 3. Add a controlled select dropdown here. */}
          {/*    It should be connected to the 'userRole' state. */}
          <option value="Guest">Guest</option>
          <option value="Editor">Editor</option>
          <option value="Admin">Admin</option>
        </label>
      </div>

      <hr />

      <h3>Current State:</h3>
      <p>Username: {/* Display username state here */}</p>
      <p>Role: {/* Display userRole state here */}</p>
    </form>
  );
}

export default ProfileEditor;

Try to complete the exercise on your own. If you get stuck, you can check the solution below.

Click to see the solution
import { useState } from 'react';

function ProfileEditor() {
  // 1. Create state for 'username' and 'userRole'.
  const [username, setUsername] = useState('');
  const [userRole, setUserRole] = useState('Guest');

  return (
    <form>
      <div>
        <label>
          Username:
          {/* 2. Add a controlled text input here. */}
          <input
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </label>
      </div>

      <div>
        <label>
          Role:
          {/* 3. Add a controlled select dropdown here. */}
          <select
            value={userRole}
            onChange={(e) => setUserRole(e.target.value)}
          >
            <option value="Guest">Guest</option>
            <option value="Editor">Editor</option>
            <option value="Admin">Admin</option>
          </select>
        </label>
      </div>

      <hr />

      <h3>Current State:</h3>
      <p>Username: {username}</p>
      <p>Role: {userRole}</p>
    </form>
  );
}

export default ProfileEditor;


Conclusion

Congratulations! You have now mastered the fundamental pattern for handling forms in React. By making React state the single source of truth, controlled components give you full power over your forms, making them more predictable and easier to debug.

Key Takeaways:

  • A controlled input has its value driven by React state.
  • This creates a "single source of truth", which is a core React principle.
  • The implementation requires two parts: binding the value prop to a state variable and updating that state with an onChange handler.
  • This pattern applies consistently to <input>, <textarea>, and <select> elements.
  • Forgetting the onChange handler on a controlled input will make it read-only.

Next Up:

In our practice exercise, we used a separate useState hook for each form input. While this works perfectly well, it can become cumbersome in larger forms. In our next lesson, we will learn a more scalable approach: how to manage multiple form inputs using a single state object.