How “Controllable” React components maximize reusability

A deep dive into what really makes React components reusable

Noam Elboim
MyHeritage Engineering

--

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.

Photo by Riccardo Annandale on Unsplash

There are 3 different ways to manage your data flow:

  1. The Uncontrolled way, where the component manages its own data.
  2. The Controlled way, where the parent manages the data.
  3. 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:

The Tabs component, holds the Banana and Apple 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 in an input element

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:

Click on any header to switch tabs

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:

  1. “Plug and Play” — using the component does not require any additional implementations. Usually almost no props and you get a lot in return.
  2. Simple to read and understand from the parent side.

Uncontrolled Cons:

  1. Hard to compose.
  2. Does not allow advanced usages, like filtering certain values or mutating them.
  3. 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:

Type in the input to switch tabs from the parent side

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:

  1. Allows easy composition.
  2. Advanced usages. Open and welcoming to data manipulations.

Controlled Cons:

  1. Cannot be used “as-is” — you must implement a state and callbacks to actually use it.
  2. 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:

Same Tabs component is used both as Uncontrolled and as Controlled

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 😇

--

--