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:
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:
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.
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:
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.
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.
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.
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} />
);
}
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:
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
.
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:
TheonChange
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 becauseonChange
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 thechange
event on the underlying input element but rather on theinput
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.
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>
);
}
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>
.
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:
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 toundefined
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?