Conventions of Controlled and Uncontrolled Components
Let’s quickly talk about some conventions that you might notice in frontend component libraries like Radix.
If you need a little more details you can take a look at My Old Post . Also a shout out to Hari’s post which helped things click after seeing the ways that component can be uncontrolled or controlled. My post aims to highlight when you’d want one flavor over another.
An implicit assumption is that the components we’re working can (depending on the props) be an uncontrolled or controlled component (not at the same time). This pattern is used in component libraries like Radix. It’s useful in situations where maybe from your requirements you make a component that is initially uncontrolled but later need some additional flexibility so you “extend” it to be controlled.
All possible combinations
You’ll find that there are 3 props that come to play
- value
- defaultValue
- onChange
Having the value
prop or not determines whether the component is
controlled or uncontrolled!
// uncontrolled
<input />
<input defaultValue="John Doe" /> // uncontrolled
<input defaultValue="John Doe" onChange="{doSomething}" />
// controlled
<input value="John Doe" /> // Bad Practice, React warning
<input value="{value}" onChange="{doSomething}" />
<input value="{value}" defaultValue="John doe" onChange="{doSomething}" />
// Bad Practice, React Warning
These are taken from Hari’s post but we’ll expand on them more below
Subtle Nuances
Bad Practice: Why Value Alone is ambiguous
At some point in your React career you might see the following error.
What it’s really trying to tell you is that if you have no onChange
prop
then the value of this component can never change.
In that case would you want this component to be read only? If yes, then
you really should explicitly set the readOnly
(exists since native HTML has readonly
).
Otherwise, that means you do want the value to be able to change so in that case
please provide an onChange
function so we can know how to do that.
Bad Practice: Default Value + Value leads to ambiguity
Another error you might see in your React career is this one
What it’s really trying to tell you is
- React considers a component with a
value
prop a controlled component - We noticed you also passed a
defaultValue
The final thing you need to notice is that there could be an ambiguous
state. What if defaultValue != value
? In this case which value should we have?
React chooses to take value
which makes the component controlled, but it
gives you a notice of this ambiguity.
Why Default Value over Value
I think one thing that seems arbitrary at first but has a surprising amount
of depth is why have defaultValue
in the following uncontrolled case?
<input defaultValue="John Doe" onChange="{...}" />
<input value="John Doe" onChange="{...}" />
The value
is a native attribute in html while defaultValue
is for JS Frontend libraries.
This is because JS Frontend have some concept of state that’s different than the
native HTML world. In these libraries, like React, state management is most of
the work. So how does React know if it should be responsible for state
or if the Browser should be? It uses value
. If value
is present then
React will take over the responsibility (making it controlled).
Otherwise, we’ll let the Browser handle things.
When to use what
Let’s go over the valid cases…
Input Variant | Reason for Use |
---|---|
<input onChange={doSomething} /> |
Uncontrolled with side effects: This allows the input to be uncontrolled, but React can still listen for changes to trigger other effects. Useful if you don’t need React to manage the value but still need to handle change events. You can use this for sitations where you might want to send HTTP requests on some input change. |
<input value={value} onChange={doSomething} /> |
Controlled (by parent component):: You want to be fully in charge of this component. You probably want to do this when some requirement of the feature wants the parent component to be able to add some customized behaviour for the component. |
<input defaultValue="John Doe" /> |
Uncontrolled with a desired starting value: Use defaultValue when you want to “guide” the component but without making it controlled and have React managing the state. A lot of really simple components like Tabs could use this one. You want the tabs to be uncontrolled and handle the logic of which tab is the active tab, but you want to say start on the second tab by default. |
<input defaultValue="John Doe" onChange={doSomething} /> |
Uncontrolled with side effects and defaultValue: Similar to the case above, we’d want this case just to listen in on the uncontrolled component and do some side effect when the data changes while providing the default value to start from. |