React Forms - Controlled Components

Chapter 22 34 mins

Learning outcomes:

  1. What are controlled components
  2. What is meant by a 'single source of truth'
  3. How to create controlled components
  4. The onChange event handler prop
  5. The special treatment given to <textarea> and <select>
  6. Setting value to null or undefined

What are controlled components?

React classifies form input components, such as <input>, <select>, etc. into two discrete categories: controlled components or uncontrolled components.

As the name suggests, controlled components are components that are controlled by React itself whereas uncontrolled components are not controlled by React.

But what exactly is at stake here? What's being controlled?

Well, the data input into the form components via the UI is what's being controlled (or not controlled).

For example, let's say we try entering the character 'g' into a text input field.

If the field represents a controlled component in React, this entered data will be provided to React which will then delegate it back to the input field if it is instructed to do so. Most importantly, the input field here doesn't itself have control of changing its data based on what's entered, as is otherwise normally the case.

On the other side of the mirror, if the input field represents an uncontrolled component in React, it will work just as input fields normally do — the character would appear in the input field in the UI immediately. React here doesn't have control over the data shown inside the input field.

So, the define it more precisely:

A controlled component in React represents a form input whose data is controlled by React.

For now, we'll concern ourselves with controlled components only; uncontrolled components will be covered in more detail in the next React Forms — Uncontrolled Components chapter.

To restate it, the term 'controlled' here comes from the fact that React controls the form input component's data, taking that control from the input itself.

Single source of truth

When conversing on controlled components, it's worthwhile discussing the related idea, or in fact the derivative idea, of a single source of truth. In this section, we shall explore what exactly is meant by this.

Consider a scenario where we enter the text 'Hello' into an input field, as shown below:

Entering 'Hello' into an uncontrolled input field.
Entering 'Hello' into an uncontrolled input field.

And now suppose that we have two sources of truth (we'll get to what this means in a while) in the underlying application:

  • One is the input field itself
  • One is the state data store managed by the app

Since the input field itself is a source of truth in the application, entering the data into the input field triggers the field to change itself which then informs the state data store to update as well. Thus we get 'Hello' displayed on the screen inside the input field.

Let's now say that we have a button to clear everything from the input field. In this case, the button's click handler will trigger the state data store to update itself which then informs the input field to update as well. Thus, we get an empty input field painted on the screen.

In this scenario, we've clearly witnessed two sources of truth.

A source of truth in this regard basically refers to any place where data resides in an app and from where signals are sent to other places to update themselves.

Here's how:

  • The input field is a source of truth because it controls whatever is input into it, i.e. it manages its own data, and then signals the state data store to update itself thereafter.
  • Similarly, the state data store is a source of truth because it also manages its own data, and then signals the input field to update as a consequence.

While having two sources of truth might allow for easier development in the short term, it ultimately might lead to complexity and difficulty managing data updates.

How? Well, it's not difficult to reason about this behavior even if we don't have any experience of a library based on two sources of truth.

When we have multiple sources of truth, it might be difficult to keep them in sync with one another. The moment we forget about this syncing, our app would have inconsistent sources of truth, leading to unexpected outputs in the UI, and difficult-to-debug cases.

React, as we just learnt above, is different from this when we're using controlled components. That is, controlled components have a single source of truth.

Let's see how this contrasts with the same input field example presented above.

Recall that the scenario is that we have entered the text 'Hello' into an input field, as follows:

Entering 'Hello' into a controlled input field.
Entering 'Hello' into a controlled input field.

This time, our input represents a controlled component and, likewise, has a single source of truth, which is simply the state data store (where exactly this data store lies is not important for now).

Because the input field is NOT a source of truth, entering the data into it does NOT trigger it to change itself; instead entering data into the field triggers the state data store to update itself, after which the state data store signals the input to update itself. And so we get 'Hello' displayed on the screen inside the input field.

Let's now say that we click on the same clear button that we talked about earlier to remove all the text from the input field. Once again, the button's click handler will trigger the state data store to update itself which subsequently informs the input field to update itself as well. Thus, we get an empty input field painted on the screen.

Notice how the input field here does NOT store any local data and doesn't have the ability to change itself based on what's entered into it.

Every single data entry into the input field is delegated to the state data store which could then update the field as it wants to. At any given point of time, our input's data resides in one logical compartment — the state data store.

We might have multiple data stores in the app, each operating in isolation in their own components, but the idea still remains the same. That is, we always have one place to turn to in order to find the latest data of a particular part of our app.

Hence we say that in React apps, specifically when we're dealing with controlled form inputs, there is a single source of truth.

Data resides in the state data store and gets transferred from there to different elements of the UI. The elements themselves don't manage any data mutations or UI updates.

With this notion of a single source of truth crisp and clear, let's now see how to actually create controlled form input components in React.

Creating a controlled component

First things first, let's create an <input> element that we'll turn into a controlled component eventually.

Here's the starting code:

function App() {
   return (
      <input />
   );
}

At this very point, if we interact with this input, like entering anything into it, it'll work normally as <input>s usually do. For instance, if we enter the character 'H', we'll get it painted on the screen immediately (as expected).

To be more specific, this <input> field is uncontrolled at the moment. (We'll explore uncontrolled components in more detail in the next chapter.)

Now in order to turn it into a controlled component, we'll need to do two things on the <input> element:

  • Set the value prop to a string
  • Set an onChange event handler

As soon as React notices one of the above on a form input, it assumes that the input also carries the other prop (value or onChange) and asserts the component to be a controlled component.

Anyways, let's now turn the <input> element above into a controlled component:

function App() {
   const [name, setName] = useState('');

   return (
      <input value={name} onChange={(e) => setName(e.target.value)} />
   );
}

The value prop is assigned the name state (since the input field is meant to obtain the name of the user) which is initialized to an empty string ('') and which gets updated by the onChange handler.

This handler accesses the data entered into the input field before the screen is painted, using target.value on the fired event.

In the latter part of this chapter, we'll see that initializing the state to a string is a must for making a component controlled.

With these two mere additions, our input component becomes a controlled component.

Now if we enter anything into the <input> element, the data will be used to update the state of App which will then be used to update the value prop of the <input> element.

Live Example

Simple.

If we omit the onChange handler, React will right away issue a warning.

Shown below is an example for omitting onChange:

function App() {
   const [name, setName] = useState('');

   return (
      <input value={name} />
   );
}
Warning: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.

The reason for this warning is evident: there is no point of setting value without onChange.

Without an onChange handler but with a value prop, there is nothing to change the state value that's ultimately assigned to an input element.

In fact, this is a pretty common source of confusion for many beginner developers who get to the code directly without worrying much about the intuition behind it and whether it would even work. (That should be avoided.)

Try executing the code above and enter anything into the input field:

Live Example

Surprisingly enough, as we enter anything into the input, we see nothing actually displayed in it.

What on Earth is happening here?

Well, as we learnt above, by virtue of the value prop, React assumes that the input here is meant to be a controlled component and so makes it one.

But because there is nothing to actually change the value of the input via the value prop, and because controlled inputs don't have control over their own data, as soon as we enter something into the field, React enters the game and resets the input to its initial value.

Honestly, that's some control!

In the next chapter, we shall see how to assign a default value to an input but not get it to transition to a controlled component, by leveraging the defaultValue prop (as even mentioned in the warning message above).

Anyways, if you're a little more curious than the average reader on how exactly the latest value from a controlled component is passed on to onChange handler but not shown in the input field, the following short snippet is for you.

Digging into the internals

Notice one technical detail in the way a controlled component provides its data to the onChange handler.

When the onChange handler gets invoked, it happens right after the input element in the DOM has been updated following the user's interaction (for e.g. typing into an input, or selecting a radio, or clicking on an option, and so on). That is, it gets invoked upon the occurrence of the input event on the input element.

Likewise, when we retrieve target.value from the event object, we get the latest value.

But now here's the climax point:

Right after the handler completes, React goes on and resets the value of the input to the value of the value prop.

So if the value prop never changes, the input never changes too. This ultimately makes the component read-only (but just functionally, not actually read-only).

The onChange event handler

Let's now get into more details of the onChange handler in React which might often confuse developers who map every event handler prop in React to the corresponding event handler property in JavaScript.

Anyone new to React would tend to assume that onChange represents the onchange event handler property in JavaScript but that's far from the truth. Instead onChange represents the oninput property from JavaScript.

That why there is a discrepancy in this naming will be explored shortly below. For now, let's review how and when does the input event fire in JavaScript and the same for the change event.

The input event fires on an input element as soon as its value changes. As simple as that. It is a mixture of a handful of events depending on the kind of input (text inputs, radios, checkboxes, selects, and so on).

In contrast, the change event fires on an <input> when it loses focus.

But when we talk about React, it improvises in this normal behavior. In particular, React calls the oninput handler corresponding to the input event in JavaScript as onChange.

There is an onInput event handler prop as well, that works similar to onChange. It's purely provided for compatibility with the existing oninput DOM property.

Besides this, React ignores the change event completely, and rightly so — there is no need of worrying about the change event since the blur event (with the onBlur prop) can be used along with a quick input value comparison to determine whether something has changed or not.

Some people feel that there was no need of making this change — dropping the change event and abstracting the input event behind onChange — while some are in favor of it.

React itself states the following in the DOM Elements page of its (legacy) docs:

The onChange event behaves as you would expect it to: whenever a form field is changed, this event is fired. We intentionally do not use the existing browser behavior because onChange is a misnomer for its behavior and React relies on this event to handle user input in real time.

Let's explain this a bit.

The name 'change' sure does mean 'whenever the value changes', and purely based on intuition, as we input into a text field, or change the currently selected radio, or check a checkbox, we are changing the value of that form input, and likewise the change event should ideally represent this action (although it doesn't currently in JavaScript).

JavaScript's change event, which fires when an input loses focus with a changed value, is a misnomer for its naming; it doesn't really align with the meaning of the word 'change'.

Again, we somewhere feel that this isn't a bad design decision per se, but yes it sure is a surprise for newcomers.

To boil it down,

  • The onChange handler prop in React does NOT get invoked upon the change event on the underlying input element but rather on the input event.
  • This is done simply to better align the meaning of the word 'change' with the underlying behavior of the corresponding event handler.

Woah, that was some discussion.

Let's now talk about two form input elements that React treats a little more specially than the casual <input> elements — <textarea> and <select>.

The <textarea> element

If you have experience of the <textarea> element from HTML, you'll know that it is a container element and that whatever value we want inside it is provided between the starting and ending tags.

Something as follows:

<textarea>Text inside the textarea.</textarea>

However, in React this is NOT the case.

The <textarea> element in React can NOT have any content inside it (or better to say, it can't have any children); the value to be assigned to a <textarea> goes in its value prop.

The following JSX code, therefore, won't work as we might expect it to:

<textarea>Text inside a textarea in JSX.</textarea>

The correct way is as follows:

<textarea value="Text inside a textarea in JSX">

The content to be placed inside the <textarea> goes inside the value prop, not between the starting and ending tags of the element.

Now you might ask "Why does React take this approach?" The answer is: consistency.

As a wrapper library working on top of the intricacies of the HTML DOM, it suits React to remove any inconsistencies where it could to ease the development process for developers. And fortunately, it does that wherever it gets the chance to.

In the case of <textarea>, by making its value to be provided via the value prop instead of via the children prop (which is what happens when we provide content between the starting and ending tags), React eases our transition between <input> and <textarea> elements — we always have the value prop to work with.

Frankly speaking, we feel that this is a good design decision by the React team.

Anyways, let's now talk about the <select> element.

The <select> element

Just like React modifies the markup-like semantics of <textarea>, the ones we're used to in HTML, it does the same for the <select> element.

In HTML, we set the preselected option in a list of options inside <select> by applying the Boolean selected attribute on that <option> element, as follows:

<select name="favorite_language">
   <option value="javascript">JavaScript</option>
   <option value="php" selected>PHP</option>
   <option value="c++">C++</option>
</select>

However, in React this is again NOT the case.

To simplify working with the <select> element, and making it consistent with how we're exposed to dealing with <input> and <textarea>, React provides a value prop on <select> as well.

Its behavior is rudimentary: the value prop on <select> contains the exact value of the <option> to select.

So for example, if we have the same <select> element as shown above and want to select the second option, we'll write the following JSX:

<select name="favorite_language" value="php">
   <option value="javascript">JavaScript</option>
   <option value="php">PHP</option>
   <option value="c++">C++</option>
</select>

Notice the value assigned to <select> — it's the exact same as the value of the third option.

Live Example

An important thing to note here is that since the <select> has value set but not onChange, changing the option would produce no effect on the screen — the component is controlled by React.

The complete implementation, with the onChange handler and a state value, follows:

function App() {
const [favoriteLanguage, setFavoriteLanguage] = useState('php'); return ( <select name="favorite_language"
value={favoriteLanguage}
onChange={e => setFavoriteLanguage(e.target.value)} > <option value="javascript">JavaScript</option> <option value="php">PHP</option> <option value="c++">C++</option> </select> ); }

Live Example

The initially selected option's value (which is the second one) is provided to useState() to create a favoriteLanguage state which is then used to set the value of the <select> element.

Pretty basic, isn't it?

Keep in mind that the value assigned to <select> must be the exact same as the value of the respective <option> element that we wish to select.

Let's try changing the value prop of <select> by adding a mere space at its end and see if React is able to match up with the correct <option>:

function App() {
   // Notice that the value 'php ' is different from the value 'php'
   // of the <option> that we wish to select below.
   const [favoriteLanguage, setFavoriteLanguage] = useState('php ');
            
   return (
      <select
         name="favorite_language"
         value={favoriteLanguage}
         onChange={e => setFavoriteLanguage(e.target.value)}
      >
         <option value="javascript">JavaScript</option>
         <option value="php">PHP</option>
         <option value="c++">C++</option>
      </select>
   );
}

Guess what, React is NOT able to match up the given value with the appropriate <option>.

Live Example

This highlights yet another paramount point to remember while working with form inputs in React. That is, we should set the value prop of <select> to the exact same value prop assigned to an <option> in order to get that option to be selected.

(opt) Setting value to null or undefined

Before we end this chapter, there is another point worthwhile mentioning. It's a little bit technical and logical, and requires a little bit of insight, so this section will be slightly longer than usual.

There sure is a lot to wrap the mind around in React. It's all simple but, arguably, quite a lot of it.

We already saw in the section above that if we omit the value prop from a form input element, and there is an onChange handler, React asserts the input to be uncontrolled.

However, what if we set the value prop to undefined or null?

This is a good question to ask and resolve. Let's think on it.

As we learnt back in the React JSX chapter, setting any prop in React to null or undefined has the consequence that the corresponding attribute is removed from the corresponding DOM element.

Henceforth, following this logic, if we set the value prop to null or undefined, React shouldn't ideally apply a value property to the corresponding input element in the DOM.

In other words, the input component should be just as if we didn't provide it a value prop, which equates to it being uncontrolled.

And guess what, setting value to null or undefined indeed makes a component uncontrolled.

But React will issue a warning when we specify the value to be null, saying to either set the value to undefined for an uncontrolled component or to '' for a controlled one, as shown below:

Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.

This is interesting. It has been the source of a bunch of long discussions on GitHub and other forums. There are mainly two things to address here:

  • Why does React issue a warning when setting null?
  • Why does setting undefined not issue a warning?

Again, if we think just for a minute or two on this carefully and deeply, we'll get to the exact reason why this works the way it does in React.

As we saw above, it's common in React apps to set the state of a form input in the parent component and then pass on that state value to the input element via its value prop.

Now there is a possibility that eventually at one point, some state change causes the state value to become null and ultimately null assigned to the value prop of the input.

What should React do in this case? Well it has two options: keep the element controlled or transition to an uncontrolled component. And based on consistency with its typical behavior, React decided to go with the latter — make the component uncontrolled.

But why is the component made uncontrolled?

Simply because normally in JSX, setting any prop to null/undefined removes that prop from the underlying DOM node.

So when we set an input element's value prop to null, we don't expect the underlying DOM node of the input to have a value attribute on it, and thus it makes sense for the input to be kept uncontrolled (since omitting value from an input element in React makes it uncontrolled).

But why does React issue a warning on null but not on undefined?

This probably has to do with how undefined and null values are typically used in code.

The following snippets expand upon both these cases.

Why does setting value to null issue a warning?

Compared to undefined, null is typically assigned to variables in JavaScript where we need to signal the absence of a concrete value explicitly.

Notice the emphasis placed on the word 'explicitly' — setting anything to null indirectly means that the user himself/herself has set this value, not the underlying code (as is the case with undefined).

React throws a warning on setting value to null because it makes an educated assumption that the code explicitly changed the value prop to null and that the user isn't possibly aware of the fact that this will be making the input element uncontrolled.

Why does setting value to undefined not issue a warning?

Variables in JavaScript applications are seldom explicitly changed to the value undefined; undefined is just the starting value of an uninitialized variable. That's it.

React doesn't throw a warning on setting the value prop to undefined because this time it makes an educated guess that the code explicitly recognizes the fact that the input element will be transitioned by React from a controlled component to an uncontrolled component and that the user is aware of this behavior.

Keep in mind that these are just assumptions made by React.

It could also be thought of this way, when providing a value to value:

  • With undefined, React assumes that the user has provided 'consent' to change the component to an uncontrolled component because setting variables to undefined explicitly isn't common in JavaScript apps.
  • With null, React assumes that the user isn't aware of the fact that the component will transition to an uncontrolled component, and thus issues a warning.

If you're blown away by this design decision of React, there's no need to worry — you're not alone. There have been a bunch of discussions around this issue that whether setting the value prop to null should make a component uncontrolled or keep it controlled.

React decided to stick to the common convention used over all kinds of props in React. That is, setting them to null or undefined results in the property being removed from the underlying DOM node.

If React were to treat value={null} to make a component controllable, it would've meant that setting a prop to null might sometimes not result in the corresponding attribute being deleted in the DOM.

All in all, this would've just lead to a little bit of inconsistency with existing behavior of React, and likewise it isn't implemented (at least at the time of this writing).

Phew! This chapter was quite a lot, wasn't it?