Data Binding in React

Introduction

So you have a bit of state in React, and you want to sync it with a form field. How do you do it?

Hand-drawn sketch. On one side, a computer memory stick holds the key/value pair “email” set to “hi@there.com”. On the other side, a text input is labeled “Email address” and has the value “hi@there.com”. An arrow from the memory stick points to the input, and an arrow from the input points to the memory stick.

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 heading
Introduction to controlled fields

So let's start with the core mechanism React uses for data binding.

Say we render an <input>:

jsx

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 value attribute:

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 0 to 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:

Same hand-drawn sketch, but updated so that the variable being tracked is “count”. An arrow connects the memory stick to the input, but the arrow connecting the input to the memory stick has been crossed out: editing the input does NOT update the state

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.
  • An onChange handler that updates the state variable when the user edits the input.

With this wired up, we have proper two-way data binding.

Same hand-drawn sketch, but updated so that the variable being tracked is “state”, and it holds the text “Hello World”.Arrows connect the memory stick and input in both directions, showing that two-way data binding has been achieved.

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.

Here's a more complete example of a text input bound to React state:

The two key attributes here are value and onChange:

  • value “locks” the input, forcing it to always display the current value of our state variable.
  • onChange is 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 heading
Text 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 password input:

jsx

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:

jsx

In React, <textarea> elements work exactly like text inputs. We use the same combo of value + onChange:

As with inputs, be sure to use an empty string ('') as the initial value for the state variable:

jsx

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 <input> tag.

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)
  • "yes" (the value of the first radio button)
  • "no" (the value of 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 onChange handler:

jsx

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 checked attribute:

jsx

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 (value vs. 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 value).
  • For a radio button, React controls whether or not the user has selected this particular option or not (specified with checked).

What about all of those other attributes? Here's a table showing what each attribute is responsible for:

Link to this heading
Iterative 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:

jsx

In our .map() call, we're naming the map parameter language, but that name is already taken! Our state variable is also called language.

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 checked attribute!

For this reason, I like to use the generic option name when iterating over possible options:

jsx

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 onChange pattern.

Link to this heading
Checkbox 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:

js

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 checked attribute.

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.)

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 value + 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 value + 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:

jsx

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 heading
Specialty 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 type="range")
  • Date pickers (with type="date")
  • Color pickers (with type="color")

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 <input type="range">:

Here's another example, with <input type="color">:

Link to this heading
Generating 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:

  1. 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.
  2. 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 heading
The journey continues!

Over the past two years, I've been working full-time on a comprehensive React course. It's called The Joy of React.

Visit the “Joy of React” homepage

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!

You can learn more about the course, and discover the joy of building with React:

Last Updated

September 19th, 2023

Hits

A front-end web development newsletter that sparks joy

My goal with this blog is to create helpful content for front-end web devs, and my newsletter is no different! I'll let you know when I publish new content, and I'll even share exclusive newsletter-only content now and then.

No spam, unsubscribe at any time.



If you're a human, please ignore this field.