How “Controllable” React components maximize reusability
A deep dive into what really makes React components reusable
How many times have you tried using a component just to find out you could not manipulate its state?
How many times did the component just not work “out of the box”?
Stop pulling your hair out — we can do something about it!
In this article, I will show you how to increase your components reusability by managing your components data flow in a better way.
There are 3 different ways to manage your data flow:
- The Uncontrolled way, where the component manages its own data.
- The Controlled way, where the parent manages the data.
- The Controllable way, where you get the best of both worlds!
Choosing the right way will have a tremendous effect on the component’s reusability.
FYI, all the examples are in React, but the discussion in this article is really about concepts pertaining to components data flow. Those concepts are easily applied to Vue.js or any other framework that support components.
Introducing Tabs
Our story is the story of Tabs:
Clicking on any tab will change the active one, indicated by a style and content change.
Let’s write a simple markup for it:
So, how do you decide which tab is active?
The Uncontrolled Way
Uncontrolled is when the component manages its own data.
Let’s look on a simple input element:<input/>
Typing text in the input will set the value
of the element with that text. Changing value
from the outside is possible only by a DOM manipulation, for example, by using React’s ref
and accessing the element’s instance.
Applying the same approach to Tabs, we get this:
How does it work?
The component has a state
(stateful component), with avalue
field in it.
To decide which child is active, we use the isSelected
function, which compares the tab (one of the children) against this.state.value
.
The isSelected
function runs in the render
and determines which header is active right now and which tab content to show.
When the user clicks on one of the headers, we will trigger the selectTab
function, which will change the state
with the new value.
We will even call a callback (this.props.onChange
) to update the parent with changes to the inner state
.
At this point, to change the active tab from the outside, you must use ref
and change the Tabs instance directly.
Therefore, it is Uncontrolled.
What Uncontrolled is good for?
Uncontrolled is mostly useful when you do not need to manipulate the data from the outside. Like, ever.
Uncontrolled Pros:
- “Plug and Play” — using the component does not require any additional implementations. Usually almost no props and you get a lot in return.
- Simple to read and understand from the parent side.
Uncontrolled Cons:
- Hard to compose.
- Does not allow advanced usages, like filtering certain values or mutating them.
- Must be a stateful component, which adds more lines of code for overall bigger, heavier components.
It is worth mentioning that Uncontrolled is usually the default pick for many developers. It is approachable, what you see in a lot of the React tutorials, and very easy to understand for beginners to React.
The Controlled Way
Controlled is when the parent manages the component data.
A Controlled input element may look something like this:<input value={valueFromParentState} onChange={this.onChange}/>
Typing text in the input will not change the element value
; it will trigger an onChange
callback with the new value. The input on the screen will change only when thevalueFromParentState
changes. Therefore we will change valueFromParentState
by the onChange
update.
To make Tabs Controlled, we will need to add the value
prop and use the onChange
callback.
Let’s build it:
How does it work?
When this.props.value
changes, it triggers a render cycle which changes the active tab. When the user clicks on one of the headers, we just execute the onChange
callback with the new value. If the parent does not do anything with the callback, nothing happens in Tabs either.
To decide which tab is active, we compare each tab’s value
to this.props.value
in the isSelected
function.
When there is a click on one of the headers, we execute this.props.onChange
with the new value.
What is Controlled good for?
Controlled components are meant to be composed in other components and are most useful that way. A Controlled component cannot do much by itself, and is dependent on the parent to control it.
Controlled Pros:
- Allows easy composition.
- Advanced usages. Open and welcoming to data manipulations.
Controlled Cons:
- Cannot be used “as-is” — you must implement a
state
and callbacks to actually use it. - Props bloating. The more Controlled components you compose, the more props you will have to maintain in each level.
The Controllable Way
What if you want the same component to be used as Uncontrolled in one place, and in another place as Controlled?
We just saw the same input element used both as Uncontrolled in the first example, and then as Controlled in the second. So it is indeed possible!
And a lot simpler than you may think.
We call it Controllable:
How does it work?
We start from the Uncontrolled version of Tabs, and our only addition is this getDerivedStateFromProps
implementation:
It binds any changes in props.value
to also happen in state.value
, which means if there was a change in the value
prop — it would be applied to the state as well.
Note how we use prevValue
to not change the state when any other prop is changing.
This mapping between prop and state is what allows the component to behave both Uncontrolled and Controlled. Though the component remains dependent on a state from the inside, we can trick it to take prop changes into account as well.
It even allows you to decide which way you want, at runtime. You may want to start as Controlled and change to Uncontrolled afterwards — all the options are available to you.
By the way, Controllable works very well in Vue.js with watch
.
makeControllable
Implementing the exact same getDerivedStateFromProps
over and over again, to make Controllable components, can be very repetitive.
That is why, my brother Tzook Shaked and I created a short util function called makeControllable to help you out.
This will shorten your getDerivedStateFromProps
implementations greatly.
You can check the full code and readme here:
https://github.com/NoamELB/make-controllable
What is Controllable good for?
Controllable has all the pros of both Uncontrolled and Controlled, and, of course, it also lets you choose between them.
To make components more reusable, you need to keep more options open. That can be achieved by making Controllable components, or at least knowing about them as a possibility, to allow an easy upgrade to Controllable when someone needs it.
Maybe you need a component to be Uncontrolled now, but the next person to use it may need it to be Controlled. With such a small addition to your code, you can have both, as immediately as you need them. Amazing, right?
Wrapping it up
If you have too many Uncontrolled components in an app, you may find yourself dragged down by way too many refs
.
If you have too many Controlled components in an app, you may find yourself managing tens of props in your high-level components.
I think there should be a balance in an app between the number of Controlled and Uncontrolled components that go into it. Achieving this balance is so much easier when you have components that can be used either way.
At MyHeritage, our ability to have Controllable components really changed the way we develop components. It opened our eyes to think more carefully about how the next person to use the component may need it, instead of just thinking about how it is needed right now.
Thank you for reading 😇