Control Forms in React is a tricky concept to learn, especially if you’re also learning about state, props, and hooks for the first time. This tutorial aims to illustrate how control forms work.
Uncontrolled Forms
An uncontrolled form contains HTML form elements (input, select, etc.) where the user’s input lives in the DOM. With uncontrolled forms, you can “pull” this data from the DOM when the form is submitted, but there’s no way of knowing what the user types as they type without a ton of explicit logic.
There’s nothing wrong with this, it’s just a lot more work for you, the coder. Fortunately for us, there’s a cool way to control the data in our forms.
Control Forms
React gives us the ability to keep track of the user’s input for dynamic use by storing data in something called state. By controlling the value of input fields of a form according to what a user types, this built-in React state is updated with every keystroke or click. A few examples of what this allows us to do might be to:
- Enforce input format, such as phone numbers or prices
- Conditionally disable buttons until certain fields are filled correctly
- Make inputs that seamlessly interact with other components
- Handle complex forms with multiple inputs for one piece of data, like an order form or newsletter signup.
React’s useState Hook
Before we get into how to make a control form, it’s important to understand what is meant by “state”. If you don’t already know how this works, I highly recommend reading React’s docs on state and managing state in React’s Beta docs before continuing.
To summarize, State is data that is dynamic in your component. It changes over time as users interact with your application, and instead of being stored in the DOM it lives in React’s internal code. We can access state using the useState hook.
All the state in your application is held in React’s internal code, not in the user’s browser. React has a useState function, or “hook”, that allows us to hook into React’s internal state inside of our function component. We’re going to use the useState hook to control our form.
To change an uncontrolled form into a controlled form, we need to:
- Set up state
- Make the input values equal to the state variable
- Set this state in a callback function in the input’s event listener
Let’s see how to make this happen. If you’d like to follow along, make sure you have a code editor of your choice, such as VS Code. Also make sure you have npm and Google Chrome installed, and create a new React app. This might take a few minutes.
In your terminal, start a new app with these steps:
npx create-react-app control-form-practice
cd control-form-practice
npm start
If all goes well, you should see the React logo rotating in your browser.
Now, in your code editor, open the App.js file found in control-form-practice/src/App.js
. Replace everything in App.js with this starter JSX:
function App() {
return (
<form style={formStyle()}>
<label>Your Name:</label>
<input type="text" />
<label>Your Age:</label>
<select>
<option value="youth">0 - 17</option>
<option value="adult">18 - 80</option>
<option value="elder">80 +</option>
</select>
<label>
If you love React, check this box =>
<input type="checkbox" />
</label>
<label>Comments:</label>
<textarea />
<button type="submit">Submit</button>
</form>
)
}
export default App;
const formStyle = () => {
return { display:"flex", flexDirection:"column", margin:"40px auto", lineHeight: "50px", width: "400px", fontSize:"20px" }
}
(I've included a formStyle() function that's used for inline styling for readability purposes in your browser. There are much better ways to style components FYI.)
Right now, this form is considered uncontrolled, and anything you type or select lives in your browser’s DOM.
To make this a Control Form, we need to incorporate state into our input, select, and textarea tags. To start, we need to add state to our component.
1. Add State
On line 1 in App.js, import the useState hook.
import { useState } from 'react';
Then, inside the App component & before the return statement (line 4), let's declare our state variables.
const [name, setName] = useState("");
const [age, setAge] = useState("young");
const [opinion, setOpinion] = useState("false");
const [comments, setComments] = useState("");
We've destructured the useState hook for each variable, each having its own state variable set to the initial value declared in useState(), and a setter function we'll use to update state every time the user interacts with the form.
2. Assign State
Next, let's add state variables to our form elements. In these elements, add the 'value' attribute and assign their state variable.
<input value={name} type="text" />
...
<select value={age}></select>
...
<input value={opinion} type="checkbox" />
...
<textarea value={comments} />
At this point, if you try to interact with our form in your DOM you'll notice each field is frozen. If you look in your browser's console, you’ll see an error message:
Warning: You provided a 'value' prop to a form field without an 'onChange' handler...
Have no fear. This is because we haven't added our setters to an event listener yet.
3. Listen for State Changes
Let's add event listeners to our form elements! The onChange event listener in React inputs expects a callback function, and has access to the event object, just like in vanilla JavaScript.
We can use event.target.value for input, select, and textarea tags. Radio buttons and checkboxes are a little different, since they're triggered by a boolean value, and can be accessed with event.target.checked. Here's what that looks like.
<input onChange={(e) => setName(e.target.value)} value={name} type="text" />
...
<select onChange={(e) => setAge(e.target.value)} value={age}>
...
<input onChange={(e) => setOpinion(e.target.checked)} value={opinion} type="checkbox" />
...
<textarea onChange={(e) => setComments(e.target.value)} value={comments} />
That's it! Now we have a simple control form. If you want to test out what we've done so far, inject our state variables into the JSX! Here's our final version of App.js, for your reference.
import { useState } from 'react';
function App() {
const [name, setName] = useState("");
const [age, setAge] = useState("young");
const [opinion, setOpinion] = useState("false");
const [comments, setComments] = useState("");
return (
<>
<form style={formStyle()}>
<label>Your Name:</label>
<input onChange={(e) => setName(e.target.value)} value={name} type="text" />
<label>Your Age:</label>
<select onChange={(e) => setAge(e.target.value)} value={age}>
<option value="youth">0 - 17</option>
<option value="adult">18 - 80</option>
<option value="elder">80 +</option>
</select>
<label>
If you love React, check this box =>
<input onChange={(e) => setOpinion(e.target.checked)} value={opinion} type="checkbox" />
</label>
<label>Comments:</label>
<textarea onChange={(e) => setComments(e.target.value)} value={comments} />
<button type="submit">Submit</button>
</form>
<h3>User Name: {name}</h3>
<h3>User Age: {age}</h3>
<h3>User Opinion: {opinion}</h3>
<h3>User Textarea: {comments}</h3>
</>
);
}
export default App;
const formStyle = () => {
return { display:"flex", flexDirection:"column", margin:"40px auto", lineHeight: "50px", width: "400px", fontSize:"20px" }
}
Top comments (1)
Thorough and clear. Very nicely written.