React Hooks: Inverting Container/Presenter

or: How I Learned to Stop Worrying and Love the Closure

--

React Hooks have fully burst forth from the class-based primordial ooze.

People cooler than I am have been using them for decades already. Who uses classes anymore? You’re still using classes?? Gross!

Shortly after their arrival, I started hearing people saying “Hooks replace the container/presenter pattern.” Dan Abramov himself updated his famed Medium article that started it all with a caveat:

I don’t suggest splitting your components like this anymore. If you find it natural in your codebase, this pattern can be handy. But I’ve seen it enforced without any necessity and with almost dogmatic fervor far too many times.

I understand his point about “dogmatic fervor,” but it’s unfortunate that he discredits such a powerful mental model for separation of concerns in React.

My first question was “How would Hooks replace container/presenter?” Okay, actually, I see it. You no longer need a container component, because the stateful logic can be contained in a custom hook. Portable statefulness, that’s pretty nice actually.

HOCs? Render props? These will likely be phased out as the adoption of Hooks grows. Hooks give you the flexibility of render props without needing to wrap your component in an extra component.

Sure, devs approaching React for the first time will have to wrap their heads around useEffect and the subtleties of the dependency array, but they won’t have to remember a litany of lifecycle methods. Remember when you were new to React? It seemed like every time you figured out a lifecycle method two others popped up hydra-like in its place. Not to mention their props— “hmm, was it prevProps/prevState or nextProps/nextState? Guess I’ll open up the ol’ devtools again.”

Anyway, where was I… The really nice thing about container/presenter is that it encourages separating your concerns. Your container handles the state/data, while your presenter — nice and pure — holds the UI, and there’s a bright line boundary between the two. Their relationship is clear: the container… contains… the presenter. It creates a natural hierarchy within the React architectural model.

Turning it upside down

What strikes me with hooks is that this relationship is now inverted. The component which was previously only responsible for the UI now… contains the container.

There doesn’t seem to be a clear way to decouple the two. It got me thinking.

You could have a stateless presentational component that you render from a stateful functional component:

// Hook
const useName = () => {
const [name, setName] = useState('DeeDee');
const onJoey = () => setName('Joey');
return { name, onJoey };
}
// Presenter
const GabbaGreeting = ({ name, onJoey }) => (
<div>
Gabba gabba hey, {name}
<button onClick={onJoey}>Click to Joey-ify</button>
</div>
);
// Ugh.
export default const NameContainer = (props) => {
const { name, onJoey } = useName();
return <GabbaGreeting name={name} onJoey={onJoey} />
}

This works, but NameContainer feels like an unnecessary layer.

And then there’s testing.

Presentational components are so easy and clear to test—pure functions, props in, UI out. They’re great for TDD, especially for devs who are just getting started with test-first development. Writing components this way really drives home how (and why) to separate your architecture into layers. How would you test the purely presentational portion of your now-stateful functional component now that the presentational components/elements are now getting props/attributes from inside the component?

One method that occurred to me is DI, injecting a custom hook into your component, but after experimenting with it a bit it just felt weird. Another possible solution is allow for setting initial values in your custom hooks, but this can get complicated…

const useForm = ({
initialValues,
initialErrors,
initialIsDirty,
initialIsLoading,
etc...
}) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState(initialErrors);
const [isDirty, setIsDirty] = useState(initialIsDirty);
const [isLoading, setIsLoading] = useState(initialIsLoading);
etc...
}

A new challenger approaches

After giving it some thought, I came up with one solution that feels like it has some potential for decoupling, testing, and brevity. I wrote a small library, react-hooks-compose, that lets you connect hooks to presentational components.

composeHooks({ useMyHook })(Presenter);

It has an API similar to connect in react-redux, mapping your hooks’ return values to props. Here’s what our first example looks like now:

// You still have your Hook
const useName = () => {
const [name, setName] = useState('DeeDee');
const onJoey = () => setName('Joey');
return { name, onJoey };
}
// You still have your Presenter
const GabbaGreeting = ({ name, onJoey }) => (
<div>
Gabba gabba hey, {name}
<button onClick={onJoey}>Click to Joey-ify</button>
</div>
);
// What have we here? Say, that's pretty easy on the eyes!
export default composeHooks({ useName })(GabbaGreeting);

It covers all 3 pain points: it keeps things decoupled, makes testing straightforward, and gets rid of that pesky extra container component.

Choose your own adventure

The more I use hooks, the more I’m … hooked (ugh). The benefits of hooks far outweigh the drawbacks, and we’re in a fun period where we can experiment and find new ways to do things. Best practices are emerging; we should let them emerge rather than impose them. New patterns will come if we keep trying, keep playing.

I’ll be honest: I disregarded the advice to not go through my projects and convert them to hooks, and learned a ton in the process. I like the consistency of “everything is just functions.” Closures are in, this is out. I prefer the ergonomics of hooks over both HOCs and render props.

Maybe I’m overthinking it, and maybe this is a non-issue. Maybe someone will come up with a better idea. Or maybe others are feeling this same pain and react-hooks-compose is the solution they’ve been looking for! All I know is right now I’m just feeling a little separation (of concerns) anxiety.

--

--

Writer for

Primarily JavaScript developer. Previously animator/illustrator. Bookworm.