The paired hook pattern

After years of working with React and TypeScript, I've seen a lot of patterns for component development, but so far I didn't see one that works as good for function components as the "paired hook pattern". To get started let's use a classic: The Counter component.

A simple example

First we write a stateless component:

const Counter = ({ count, onDecrement, onIncrement }) => (
	<>
		<span>{count}</span>
		<button onClick={onIncrement}>+</button>
		<button onClick={onDecrement}>-</button>
	</>
);

And when we use it, we need to create a state for it:

const App = () => {
	const [count, setCount] = useState(0);

	return (
		<Counter
			count={count}
			onDecrement={() => setCount(count - 1)}
			onIncrement={() => setCount(count + 1)}
		/>
	);
};

The dynamic looks something like this:

Diagram showing how the state and the component interact with each other with events and props, and they are both contained in the app

The first problem: Reuse

The problem with the stateless component is that we need to use the useState hook every time we use the component, which might be annoying for components that require more properties and are all over your app.

So, is pretty common to just put the state directly in the component. Doing this we don't need to have a state every time we use it, so then our Counter component changes to something like this:

const Counter = ({ initialCount = 0, step = 1 }) => {
	const [count, setCount] = useState(initialCount);

	return (
		<>
			<span>{count}</span>
			<button onClick={() => setCount(count + step)}>+</button>
			<button onClick={() => setCount(count - step)}>-</button>
		</>
	);
};

And then to use it, as many times as we want without having to create a state for each:

const App = () => (
	<>
		<Counter />
		<Counter />
		<Counter />
	</>
);

The dynamic then looks like this:

Diagram showing 3 components, each with its state inside, all wrapped by the app

The second problem: Data flow

Now, that's great until we want to know the current state of the counter element from the parent element. So you might be tempted to create a monster like this one:

const Counter = ({ initialCount = 0, step = 1, onCountChange }) => {
	const [count, setCount] = useState(initialCount);

	useEffect(() => onCountChange?.(count), [count]);

	return (
		<>
			<span>{count}</span>
			<button onClick={() => setCount(count + step)}>+</button>
			<button onClick={() => setCount(count - step)}>-</button>
		</>
	);
};

And then use it like this:

const App = () => {
	const [count, setCount] = useState(0);

	return (
		<>
			<span>Current count in Counter: {count}</span>
			<Counter onCountChange={setCount} />
		</>
	);
};

It might not be obvious at first, but we are introducing side effects to every state change just to keep the parent in sync with the children, and this has two significant issues:

  1. The state is living in two places at once (the parent element and the children).
  2. The children are updating the state of the parent, so we are effectively going against the one-way data flow.

Diagram that shows a child element updating the parent state through side effects

The paired hook pattern

One of the best things about hooks is when we create our own. The solution I propose for this issue is quite simple, but I honestly believe solves the vast majority of issues with state I've seen around. The first step is similar to what we had at the beginning here, we just create a stateless component:

const Counter = ({ count, onDecrement, onIncrement }) => (
	<>
		<span>{count}</span>
		<button onClick={onIncrement}>+</button>
		<button onClick={onDecrement}>-</button>
	</>
);

But this time, instead of requiring the consumers of our component to figure out the state themselves, we create a hook that goes together with our component, we can call it useCounter. The main requirement for this hook is that it needs to return an object with properties matching the properties of Counter:

const useCounter = ({ initialCount = 0, step = 1 } = {}) => {
	const [count, setCount] = useState(initialCount);

	return useMemo(
		() => ({
			count,
			onDecrement: () => setCount(count - step),
			onIncrement: () => setCount(count + step),
		}),
		[count, step],
	);
};

What this enables is that now we can use it almost as a stateful component:

const App = () => {
	const counterProps = useCounter();

	return <Counter {...counterProps} />;
};

But also we can use it as a stateless component:

const App = () => <Counter count={42} />;

And we no longer have limitations accessing the state, because the state is actually in the parent.

const App = () => {
	const { count, ...counterProps } = useCounter();

	return (
		<>
			<span>Current count in Counter: {count}</span>
			<Counter {...{ count, ...counterProps }} />
		</>
	);
};

The dynamic then looks something like this:

Diagram showing how the paired hook of the component interacts with it similarly to how the state did previously

With this approach, we are truly making our component reusable by not making it require a context or weird callbacks based on side effects or anything like that. We just have a nice pure stateless component, with a hook that we can pass directly or just partially if we want to take control of any property in particular.

The name "paired hook" then comes from providing a hook with a stateless component that can be paired to it.

A problem (and solution) with the paired pattern

The main issue the paired hook approach has is that now we need a hook for every component with some kind of state, which is fine when we have a single component, but becomes tricky when we have several components of the same type (like for example having a list of Counter components).

You might be tempted to do something like this:

const App = ({ list }) => (
	<>
		{list.map(initialCount => {
			const counterProps = useCounter({ initialCount });

			return <Counter {...counterProps} />;
		})}
	</>
);

But the problem with this approach is that you're going against the rules of hooks because you're calling the useCounter hook inside a loop. Now, if you think about it, you can loop over components that have their own state, so one viable solution is to create a "paired" version of your component, which calls the hook for you:

const PairedCounter = ({ initialCount, step, ...props }) => {
	const counterProps = useCounter({ initialCount, step });

	return <Counter {...counterProps} {...props} />;
};

// And then...
const App = ({ list }) => (
	<>
		{list.map(initialCount => (
			<PairedCounter initialCount={initialCount} />
		))}
	</>
);

This approach seems similar to the stateful approach (the second example in this article) but is way more flexible and testable. The other approach we have is to create a component context for every item without having to write a component ourselves, and for that, I created a small function that I published in npm called react-pair:

React Pair logo

The function is so simple, you could write it yourself, the only difference is that I'm testing it, adding devtools integration, and typing with TypeScript for you. You can check the source here. The usage is quite simple, react-pair provides a pair function that you can use to create a component that gives you access to the hook in a component context (without breaking the rules of hooks):

import { pair } from "react-pair";
import { useCounter } from "./useCounter";

const PairedCounter = pair(useCounter);

const Component = ({ list }) => (
	<ul>
		{array.map((initialCount, index) => (
			<PairedCounter key={index}>
				{usePairedCounter => {
					const counterProps = usePairedCounter({ initialCount });

					return <Counter {...counterProps} />;
				}}
			</PairedCounter>
		))}
	</ul>
);

Just to be clear, you don't need to use react-pair to achieve this, you can just create a new stateful component by hand, that just pairs the hook with the component.

Either if you use the util or not, the resulting dynamic looks something like this:

Diagram showing several component+hook pairs at the same level

We get something similar to the stateful approach but with less coupling and more flexibility, because the state doesn't live inside the component, it lives "besides" it. So we have the cake and eat it too 🍰

TL;DR

  • Write a stateless component, designed to work in isolation.
  • Write a custom hook to be paired with that component.
  • Use the component with the hook for a stateful experience.
  • Use the component without the hook for a stateless experience.
  • Use the component with just a few properties from the hook for a mixed experience.
  • Use an util or a wrapper component when looping.
  • If you can avoid state altogether, do it, but if you really have to have state in your component, better do it in a clean and decoupled way.

Closing thoughts

I've been using this pattern for a while now and so far I didn't found any blocking issues with it, so I invite you to try it out in one of your projects and tell me how it goes!

Special thanks to y'all 3500+ followers that keep motivating me to write these blog posts. You're the best ✨