Let’s talk about controlled vs uncontrolled components and why gambling with them can be dangerous. Incorrect usage may lead to unexpected behaviors like breaking the single source of truth principle.
An input will be controlled whenever we declare the
value attribute (as opposite to uncontrolled component, which makes use of
defaultValue). This let ReactJS take total control over the input.
The input value won’t be updated if the component state is not updated. If we comment out the
_onChange method we won’t be able to modify the input, even though the
event is still being fired.
This give us great flexibility, like the chance to pre-process the value, for example if we want to enforce upper case only and limit the length to five characters we can easily achieve that by refactoring the
Imagine that we are writing a chat application and we want to show a
John Doe is typing.. message to the other participants; that would be a breeze to achieve thanks to the onChange handler. In practice, however, we won’t always need such fine-grained level of control.
Beware of mad component #
Something to be careful about controlled components is that we can end up breaking the single source of truth principle. The following component let us update our full name and will apply a nice formatting to it, so if we enter “ivanNa HUMpalOT” it will be converted to “Ivanna Humpalot”:
Did you spot the issue? The Dashboard’s
_updateUsername method will successfully convert the input and update the Dashboard’s state as we can see in the welcome message, but the form won’t reflect it because it handles its own state and uses
this.props.initialUsername for initialization only. (Throwing props into a component’s state is an antipattern too, yikes!).
The sad part of the story is that the new initialUsername value is still being passed down to the Profile component after the Dashboard state has been updated, but the component no longer makes use of it.
Inadvertently we have broken the single source of truth principle, because the value for the username now lives inside the Dashboard state and inside the Profile state as well, and we should avoid this bad practice at all cost.
In our example, this issue might be easy to spot after one or two re-reads, but in an app with many layers of sub-components, and with many historical refactors this bug will await patiently to show up in the worst moment. And believe me, this is hard to debug.
Using a controlled input component also implies adding an
onChange handler that without a meaningful purpose it quickly becomes lousy code. If a controlled component achieves no more than an uncontrolled one, please save yourself from the hassle and use the latter.
To avoid breaking the single source of truth principle the rule of thumb is that an application data element should live in one and only one component state. If we need different representations of the same data, then the solution is - besides keeping it in a single state - to let other components to decorate it only.
A better solution #
Hopefully the inline comments will point out the major changes in our code. In short, the Profile component no longer handles the username state, rather it’s a channel between the end user and the Dashboard component. However notice the
editing attribute is still being part of the Profile, maybe we agree that the Dashboard won’t benefit at all from knowing about the inner cogs of the Profile.
The value is being passed raw from the Profile to the Dashboard’s
_updateUsername handler, and is being saved raw into the state as well. The render method makes use of the _ucwords function to give the username a nicer display representation on the fly.
Controlled vs uncontrolled form components is a very simple concept, yet we may find ourselves failing to choose wisely if we are not careful enough to foresee big upcoming refactors where forms needs to be split into smaller and more abstract pieces.
Have you faced these issues at all? Please share your experience with forms in the comments sections below. If you have any question write them down too. Thanks for reading.