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.

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:
- The
valueprop: You pass a state variable to the input'svalueprop. This forces the input's displayed value to always match your React state. (State → UI) - The
onChangehandler: 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 atevent.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.
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:
- A text input for a
username. - A
<select>dropdown for auserRolewith 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
valueprop to a state variable and updating that state with anonChangehandler. - This pattern applies consistently to
<input>,<textarea>, and<select>elements. - Forgetting the
onChangehandler 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.