React Controller Components

Controller components are a render-prop based pattern that can help you decouple state from presentation, and that facilitates reuse of business logic.

31st August, 2018

When React first appeared on the scene, it was seen as a view library. It said so on the website: “the V in MVC”! But with the growth of render props, the view isn’t the whole story anymore. In fact, some components don’t reference the DOM at all!

As it happens, many of these non-view components tend to follow the same pattern, which I’ve dubbed the controller component pattern. They:

There are a number of well-known controller components — you may have already used some! The <Consumer> component in React’s Context API is a controller component, as is the <Route> component in react-router 4.

Here’s how a controller component would typically be used:

render(){return(<AuthController>{output =>// Each time the output changes, this function will be// called to get the controller's new children.<div>{output.name}</div>}</AuthController>)}

If you’re familiar with the distinction between presentation and container components — controller components are like renderless containers. They manage data and behavior, but instead of passing it to other containers or presentation components within render(), they just pass it to the children() function.

What if you want the state to stick around for next time the component is displayed?

For a concrete example, let’s say that you’ve been tasked with building an app with two tabs. And one of these tabs must contain a sortable <Table />.

Your first instinct may be to handle the sorting and filtering within the Table component itself. And this works great — until the user switches tabs, unmounting the table component. When the user navigates back to the first tab, her filters have disappeared!

In order to keep the user’s input around after the table has been unmounted, you’ll need to move the state into a component at a high enough level that it isn’t unmounted when the user switches tabs — and then pass it down via props. Within the React community, this process is often called “lifting state up”.

Problem solved, right? Lifting up state is a fine solution in a small demo like this. But imagine for a moment that you’re working on a real-world app, with lots of tables, lots of charts, and one massive component somewhere that handles all of this… or actually, don’t imagine that. It’ll probably give you nightmares.

One of the hidden benefits of storing state close to where it is used is that it gives you well structured state for free. Your component state has the same structure as your component tree. But once you start lifting state up… that structure disappears.

This dichotomy between ephemeral, structured state and enduring, unstructured state is the reason for all the consternation about “state management” within the React community. As apps grow, more state needs to be lifted up, and things started to get crowded. Cue tools for structuring state.

As you may have guessed, Redux is one approach to structuring state. And for truly top-level state — things like cached requests or drag-and-drop state — it’s a great solution. But Redux presents its own problems, and forces you to leave everything you know about React at the door. If you just want to store some filters, it’s overkill.

A simpler way to manage your table’s state is to output it from a controller component, and pass it down to the <Table /> element via props. Here’s how:

Moving your component’s logic into a controller component has the bonus of making it more reusable.

To continue with the table example, imagine that your task has suddenly changed (as tasks inevitably do), and now you need two tables — and one of them should always have the “TODO” filter.

Because your table’s logic is all bundled up in a component, reusing it is simple as adding another instance of that component. And because the state is all exposed on the output object, adding the extra “TODO” filter is simple!

As you start to use more controllers, you’ll soon run into a problem reminiscent of callback pyramids. I like to call these controller mountains. In fact, if you’ve been using the Context API, you may have already created a controller mountain or two!

Luckily, controller mountains are easily dealt with using the <Combine> component from the react-controllers package. This component expects that each of its props is a function that returns a controller element:

Controllers are incredibly useful. Combined with the Context API, they can go a long way to structuring your higher-level state. However, controllers do have an Achilles heel: they’re a footgun when combined with shouldComponentUpdate or PureComponent. To experience this first-hand, try figuring out why the “Increase counter 1” button doesn’t work in this example, while the “Increase counter 2” button does:

Because controllers are just plain old React components, they’re re-rendered each time their parent is called. And because the result of their children() function is returned from render(), the children() function won’t be called if shouldComponentUpdate() returns false — even if the parent has passed something new into children!.

You should never, ever use shouldComponentUpdate() or PureComponent with a controller.

Controllers can be hard to optimize, and putting controllers near the top of a large application can cause performance problems. But in small apps, you’ll have new problems. And even in large apps, a few well-placed controllers aren’t going to send you to performance hell.

Premature optimization is the root of all evil.

— Donald Knuth

Of course, you don’t need to build your entire app from controllers to benefit from improved reusability and code clarity. And since you’ll be using the Context API one way or another, learning the ins and out of controllers can only be a good thing!

That’s it for this episode of React Render Props Patterns, but stay tuned for the more…

Go Pro

Stay in the loop.

Keeping up to date with React is a full time job that pays only in frustration. Luckily, you can delegate! Just become a free member, and we'll keep you up to date with our monthly newsletter.