So you have a bit of state in React, and you want to sync it with a form field. How do you do it?
Well, it depends on the type of form control: text inputs, selects, checkboxes, and radio buttons all work a little bit differently.
The good news is that while the details vary, they all share the same fundamental mechanism. There's a consistent philosophy in React when it comes to data binding.
In this tutorial, we'll first learn how React approaches data binding, and then I'll show you how each form field works, one by one. We'll look at complete, real-world examples. I'll also share some tips I've learned over the years, and some “gotchas” to watch out for!
Link to this headingIntroduction to controlled fields
So let's start with the core mechanism React uses for data binding.
Say we render an
By default, React takes a very “hands off” approach. It creates the
<input> DOM node for us and then leaves it alone. This is known as an uncontrolled element, since React isn't actively managing it.
Alternatively, however, we can choose to let React manage the form field for us. For text inputs, we opt in using the
Try and edit the text in the input. It doesn't work!
This is known as a controlled element. React is on guard, making sure that the input always displays the string "Hello World".
Now, it's not super useful to lock the
value to a static string like this! I'm doing it here purely to illustrate how controlled elements work: React “locks” the input so that it always contains the
value we passed in.
The real magic happens when we pass a dynamic value. Let's look at another example:
Try clicking the “Increment” button, and notice what happens to the text input. 😮
Instead of binding the input to a static string, we've bound the input to a state variable,
count. When we click the "Increment" button, that state variable changes from
1. React re-renders this component, and updates the value in the
<input> to reflect this new reality.
We still can't type in the text input, though! React is keeping the input locked to the value of the
count state variable.
In data-binding lingo, this is known as "one-way" data binding. The input updates when the state changes, but the state doesn't update when the input is edited:
To complete the loop, we need two-way data binding. Here's how we accomplish it:
We attach an event listener with the
onChange attribute. When the user edits the text input, this function is invoked, and the
event object is passed in.
event.target is a reference to the DOM node that triggered the event: in this case, it's the text input. That text input has a
value attribute, and this represents the value that the user has just tried to enter into the input.
We update our React state so that it holds this new value. React re-renders, and pushes that new value into the input. The cycle is complete!
This is the fundamental idea behind data binding in React. The two ingredients are:
- A “controlled” field that locks the input to a piece of React state.
onChangehandler that updates the state variable when the user edits the input.
With this wired up, we have proper two-way data binding.
One of the core philosophies in React is that the UI is derived from state. When the state changes, the UI is redrawn to match. Controlled elements are a natural extension of this idea. By specifying a
value for a text input, for example, we're saying that the input's content is also derived from React state.
Alright, let's look at how this pattern is applied across different input types.
Link to this headingText inputs
Here's a more complete example of a text input bound to React state:
The two key attributes here are
value“locks” the input, forcing it to always display the current value of our state variable.
onChangeis fired when the user edits the input, and updates the state.
I'm also providing an
id. This isn't required for data binding, but it's an important usability and accessibility requirement. IDs need to be globally-unique; later, we'll learn how to generate them automatically using a new React hook.
Link to this headingText input variants
In addition to plain text inputs, we can pick from different “formatted” text inputs, for things like email addresses, phone numbers, and passwords.
Here's the good news: These variants all work the same way, as far as data binding is concerned.
For example, here's how we'd bind a
In addition to text input variants, the
<input> tag can also shape-shift into entirely separate form controls. Later in this blog post, we'll talk about radio buttons, checkboxes, and specialty inputs like sliders and color pickers.
When working with text inputs, be sure to use an empty string (
'') as the initial state:
Link to this headingTextareas
<textarea> elements work exactly like text inputs. We use the same combo of
As with inputs, be sure to use an empty string (
'') as the initial value for the state variable:
Things are a bit different when it comes to radio buttons!
Let's start with an example:
Phew, that's a lot of attributes! We'll break them down shortly, but first, I want to explain how our “controlled field” strategy applies here.
With text inputs, there's a 1:1 relationship between our state and our form control. A single piece of state is bound to a single
With radio buttons, there are multiple inputs being bound to a single piece of state! It's a 1:many relationship. And this distinction is why things look so different.
In the example above, our state will always be equal to one of three possible values:
undefined(no option selected)
valueof the first radio button)
valueof the second radio button)
Instead of tracking the value of a specific input, our state variable tracks which option is ticked.
We can see this at work in the
When the user ticks this particular input (which represents the “yes” option), we copy that “yes” value into state.
For true two-way data-binding, we need to make this a controlled input. In React, radio buttons are controlled with the
By specifying a boolean value for
checked, React will actively manage this radio button, ticking or unticking the DOM node based on the
hasAgreed === "yes" expression.
It's unfortunate that text inputs and radio buttons rely on different attributes for establishing a controlled input (
checked). This leads to a lot of confusion.
But it sorta makes sense when we consider what React is actually controlling:
- For a text input, React controls the freeform text that the user has entered (specified with
- For a radio button, React controls whether or not the user has selected this particular option or not (specified with
What about all of those other attributes? Here's a table showing what each attribute is responsible for:
Link to this headingIterative example
Because radio buttons require so many dang attributes, it's often much nicer to generate them dynamically, using iteration. That way, we only have to write all this stuff once!
Also, in many cases, the options themselves will be dynamic (eg. fetched from our backend API). In these cases, we'll need to generate them with iteration.
Here's what that looks like:
This might look quite a bit more complex, but ultimately, all of the attributes are being used in exactly the same way.
When using iteration to dynamically create radio buttons, we need to be careful not to accidentally “re-use” a variable name used by our state variable.
Avoid doing this:
.map() call, we're naming the map parameter
language, but that name is already taken! Our state variable is also called
This is known as “shadowing”, and it essentially means that we've lost access to the outer
language value. This is a problem, because we need it to accurately set the
For this reason, I like to use the generic
option name when iterating over possible options:
Link to this headingCheckboxes
Checkboxes are very similar to radio buttons, though they do come with their own complexities.
Our strategy will depend on whether we're talking about a single checkbox, or a group of checkboxes.
Let's start with a basic example, using only a single checkbox:
As with radio buttons, we specify that this should be a controlled input with the
checked property. This allows us to sync whether or not the checkbox is ticked with our
optIn state variable. When the user toggles the checkbox, we update the
optIn state using the familiar
Link to this headingCheckbox groups
Things get a lot more dicey when we have multiple checkboxes that we want to control with React state.
Let's look at an example. See if you can work out what's happening here, by ticking different checkboxes and seeing how it affects the resulting state:
In terms of the HTML attributes, things look quite similar to our iterative radio button approach… But what the heck is going on with our React state? Why is it an object?!
Unlike with radio buttons, multiple checkboxes can be ticked. This changes things when it comes to our state variable.
With radio buttons, we can fit everything we need to know into a single string: the
value of the selected option. But with checkboxes, we need to store more data, since the user can select multiple options.
There are lots of ways we could do this. My favourite approach is to use an object that holds a boolean value for each option:
In the JSX, we map over the keys from this object, and render a checkbox for each one. In the iteration, we look up whether this particular option is selected, and use it to control the checkbox with the
We also pass a function to
onChange that will flip the value of the checkbox in question. Because React state needs to be immutable, we solve this by creating a near-identical new object, with the option in question flipped between true/false.
Here's a table showing each attribute's purpose:
(We can also specify a
name, as with radio buttons, though this isn't strictly necessary when working with controlled inputs.)
Link to this headingSelect
Like radio buttons, the
<select> tag lets the user select one option from a group of possible values. We generally use
<select> in situations where there are too many options to display comfortably using radio buttons.
Here's an example showing how to bind it to a state variable:
In React, <select> tags are very similar to text inputs. We use the same
onChange combo. Even the
onChange callback is identical!
If you've worked with
<select> tags in vanilla JS, this probably seems a bit wild. Typically, we'd need to dynamically set the
selected attribute on the appropriate
<option> child. The React team has taken a lot of liberties with
<select>, sanding off the rough edges, and letting us use our familiar
onChange combo to bind this form field to some React state.
That said, we still need to create the
<option> children, and specify appropriate values for each one. These are the strings that will be set into state, when the user selects a different option.
As with text inputs, we need to initialize the state to a valid value. This means that our state variable's initial value must match one of the options:
This is a smelly fish. One small typo, and we risk running into some very confusing bugs.
To avoid this potential footgun, I prefer to generate the
<option> tags dynamically, using a single source of truth:
Link to this headingSpecialty inputs
As we've seen, the
<input> HTML tag can take many different forms. Depending on the
type attribute, it can be a text input, a password input, a checkbox, a radio button…
In fact, MDN lists 22 different valid values for the
type attribute. Some of these are “special”, and have a unique appearance:
- Sliders (with
- Date pickers (with
- Color pickers (with
Fortunately, they all follow the same pattern as text inputs. We use
value to lock the input to the state's value, and
onChange to update that value when the input is edited.
Here's an example using
Here's another example, with
Link to this headingGenerating unique IDs
In each of the examples we've seen, our form fields have been given an
id attribute. This ID uniquely identifies the field, and we use it to wire up a
<label> tag, linked using
htmlFor (React's version of the “for” attribute).
This is important for two reasons:
- Accessibility. Form fields require labels; without them, how would the user know what to enter? For folks who use screen readers?, proper wiring is required to make sure they're aware of the label for every given form field.
- Usability. Wiring up a label allows the user to click the text to focus / trigger the form control. This is especialy handy for radio buttons and checkboxes, which are often too small to easily click.
For everything to work properly,
id attributes should be globally unique. We're not allowed to have multiple form fields with the same ID.
But! One of the core principles in React is reusability. We might want to render a component containing form fields multiple times on the same page!
To help us square this circle, the React team recently unveiled a new hook: useId. Here's what it looks like:
Whenever we render this
LoginForm component, React will generate a new, guaranteed-unique ID. You can learn much more about this hook over on the new React docs.
Link to this headingThe journey continues!
Over the past year, I've been working full-time on a comprehensive React course. It's called The Joy of React.
I started using React professionally back in 2015, and I've been working with it ever since. Over the years, I've been building my mental model one piece at a time. These days, I feel very comfortable with the tool, and as a result, it's an absolute joy to use.
I've tried a bunch of other front-end libraries: Angular, Vue, Svelte. Ultimately, though, I just really enjoy building web applications with React!
My goal with The Joy of React is to help you build that robust mental model, to teach you how the tool truly works, so you can avoid all of the common stumbling blocks. I'll show you how to build some really cool stuff, and we'll have a lot of fun along the way!
This course will be released in full later this year. Learn more about it on the course homepage:
March 20th, 2023