DEV Community

Cover image for The paired hook pattern
Luke Shiru for VANGWARE

Posted on • Updated on • Originally published at luke.sh

The paired hook pattern

Discussion was locked because I moved away from DEV to blog on my personal website. If you want to chat about the content of any article, hit me up in Mastodon.


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>
    </>
);
Enter fullscreen mode Exit fullscreen mode

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)}
        />
    );
};
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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

const App = () => (
    <>
        <Counter />
        <Counter />
        <Counter />
    </>
);
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

And then use it like this:

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

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

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>
    </>
);
Enter fullscreen mode Exit fullscreen mode

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],
    );
};
Enter fullscreen mode Exit fullscreen mode

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

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

    return <Counter {...counterProps} />;
};
Enter fullscreen mode Exit fullscreen mode

But also we can use it as a stateless component:

const App = () => <Counter count={42} />;
Enter fullscreen mode Exit fullscreen mode

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 }} />
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

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} />;
        })}
    </>
);
Enter fullscreen mode Exit fullscreen mode

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} />
        ))}
    </>
);
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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 ✨

Top comments (12)

The discussion has been locked. New comments can't be added.
Collapse
 
jonsilver profile image
Jon Silver

That's a pretty elegant pattern, @lukeshiru. Once you start composing primitive custom hooks together to make the paired hook for a component, this starts to look like a candidate for best practice. I'd probably stick both parts of the pair into the same module as named exports, to reinforce their pairness.

Avoiding double-binding, storing only a single version of state in the place where it's most appropriate to store it... this is where React devs should all be headed. I've seen and resolved too many run-away render loops caused by storing duplicate state where it doesn't belong... anything that helps a dev keep a clear picture of data flow is very welcome.

Collapse
 
lukeshiru profile image
Luke Shiru

Hi Jon! This pattern came from the fact that I tend to avoid state as much as possible in my components, because that makes them easy to test, easy to maintain and predictable. I still believe the best approach is to design your components as if there was no state at all, by receiving everything trough props and letting the outside world know what happen internally trough events.

The thing is that at the end of the day we need state to have an app that actually does something 🤣 so I came up with the paired pattern to provide a state when needed, and still get all the benefits of stateless components.

A little history of how that came to be inside this spoiler if you want to read 😅

My first approach was similar to what Christian suggested in another comment, but I noticed that I was basically changing stateless components into components that "need a state" to make sense, having events designed to set state was really bad, and I really didn't wanted to change my beautiful stateless components. So the next step was creating a variant of the same component but with state, something like:

const StatefulCounter = ({ initialCount, step, ...props }) => {
    const [count, setCount] = useState(initialCount);

    return (
        <Counter
            count={count}
            onDecrement={() => setCount(count - step)}
            onIncrement={() => setCount(count + step)}
            {...props}
        />
    );
};
Enter fullscreen mode Exit fullscreen mode

That one was great, but it had 2 issues:

  1. I need to create a Stateful version of every component by hand.
  2. Is still kinda hard to know what the state is from the outside world.

So the next thing was the one that gave origin to the paired hook pattern: What if we create a custom hook for the component's state? I hit a wall when I noticed that in loops that wouldn't work, but then I came up with a code that derived from the snippet above and I already shared in the article:

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

    return <Counter {...counterProps} {...props} />;
};
Enter fullscreen mode Exit fullscreen mode

And the last step was the pair util that "automates" that process so we don't have to manually write every paired component.


Thanks for taking the time of reading the article and commenting!

Collapse
 
paratron profile image
Christian Engel

I would not use this pattern, heres why:

With those paired hooks, you create tight coupling. You need to import and place the exact right hook in order to make your component work correctly.

You also make things harder to understand, because instead of the basic useState, a custom hook is required, which returns something, an opaque object. You then spread that object onto the component to make it work.

I am very much in favor of stateless components which follow this pattern: They expose a value prop and an onChange prop. No matter if its an text input, a counter, a datepicker. This way, you always know how to use the components.

Then, an ordinary useState can be applied.

const [state, setState] = useState(0);

return <Counter value={state} onChange={setState} />
Enter fullscreen mode Exit fullscreen mode

The mechanics of the counter are completely hidden away and you dont have to care about it from outside. A value goes in, a value comes out. No special namigs, no custom hooks.

Collapse
 
lukeshiru profile image
Luke Shiru • Edited

Hi Christian! Your suggested approach has some issues I ran into a few times that are pretty common in React projects, but let's go little by little to make it as clear as possible:

With those paired hooks, you create tight coupling.

The exact point of the paired hook pattern is to reduce the coupling while maintaining the benefits of coupled code. As I mentioned in the article stateless components are the ideal solution for pretty much everything, but now and then the ergonomics of said components end up producing a bad DX. With a paired hook we get "the best of both worlds".

You need to import and place the exact right hook in order to make your component work correctly.

Not quite, you can for example use the same hook for two different components that share properties, or for two instances of the same components located in different places in the same render, and so on. Not to mention that you don't have to use that paired hook, you can use the component stateless as well, or even have different paired hooks for the same component.

You also make things harder to understand, because instead of the basic useState, a custom hook is required, which returns something, an opaque object.

The idea is to use naming conventions so the paired hook always reflects the component is meant to be used with. And the object is not "opaque" if it is expected to provide the component properties (not to mention that if you work with TS, is even more predictable).

I am very much in favor of stateless components which follow this pattern: They expose a value prop and an onChange prop. No matter if its an text input, a counter, a datepicker. This way, you always know how to use the components.

You can use that and have a paired hook if you want to make it "stateful" as well.

const [state, setState] = useState(0);

return <Counter value={state} onChange={setState} />
Enter fullscreen mode Exit fullscreen mode

The problem with that approach is that Counter then is going against one-way data flow by updating the state of the parent directly. This is not a good approach because then the state becomes unpredictable (children could update the state of the parent whenever they want and produce unpredictable effects).

The mechanics of the counter are completely hidden away and you dont have to care about it from outside. A value goes in, a value comes out. No special namigs, no custom hooks.

You need to know what the value type is, so is not "completely hidden", and the problem as I mentioned in the previous part of my response is your "a value comes out". This is one step away from being a "double binding", and we learn a long time ago that double bindings aren't great ... especially in React. Not to mention that you said you don't want coupling, but you're designing your component events to set a state.

Thanks for your detailed comment!

Collapse
 
lukeshiru profile image
Luke Shiru

Psst! If you prefer preact, I also made preact-pair:

Preact Pair logo

Collapse
 
nasheomirro profile image
Nashe Omirro • Edited

The pattern basically provides a default behavior for stateless components, or honestly just a set of state and functions in general. While we can just pass the state and it's setter function, we usually write functions for the stateless component to call.

// this let's counter do whatever it wants with the parent state
const Counter = ({ value, onChange }) => {
  // ...
}

// pass functions instead of manually changing the parent state.
const Counter = ({ count, onIncrement, onDecrement }) => {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

And in the parent component, we can do something like:

const Parent = () => {
  const [count, setCount] = useState(0);
  return (
    <Counter 
      count={count}
      onIncrement={() => setCount(prev => prev + 1)}
      onDecrement={() => setCount(prev => prev - 1)}
    >
  )
}
Enter fullscreen mode Exit fullscreen mode

The pattern is just taking the logic from the Parent and into a hook:

const useCounter = () => {
  const [count, setCount] = useState(0);
  return {
    count,
    onIncrement: () => setCount(prev => prev + 1),
    onDecrement: () => setCount(prev => prev - 1),
  }
}

const Parent = () => {
  const { count, ...counterProps } = useCounter();
  return (
    <Counter count={count} {...counterProps}/>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now the actual reason for you to use this pattern isn't about reusability or data-flow as the article suggests, it's whether you need default props for your Counter component, for example, if you have this Counter in many places in your app which all have the same behavior.

Another thing, the second problem is trivial, if you need the parent component to know the child component's state then just lift the state up.

Collapse
 
lukeshiru profile image
Luke Shiru

You're kinda in the right track, but not quite. For example, your first code snippet has one of the examples in the article itself and one pretty common approach that from my point of view is "pretty bad" as well:

// This one is in the article as a stateless approach
const Counter = ({ count, onIncrement, onDecrement }) => {};

// `value `is ok, but `onChange` might be called from two different
// buttons internally, and which one was it is kinda important for this component
const Counter = ({ value, onChange }) => {};
Enter fullscreen mode Exit fullscreen mode

In a future article I'll go more in detail on why the second example there is not so good, but let's say that is not "honest" and that's not good. Don't get me wrong, is still better than having a stateful component, but I think the interface could be a little better.

Your second code snippet is also in the article, and is just the way we handle state of a stateless component, by having the state on the parent itself.

The pattern is just taking the logic from the Parent and into a hook

Yes, but not quite. The pattern is taking the logic particular to a component into a hook, but that state is usually moved by devs inside the component itself to avoid having to set it up every time in the parent, so is more like we are moving the state from the Child instead of from the Parent. I went in that order in the article because is what devs tend to do:

  1. Write a stateless component and make the parent handle the state.
  2. Notice that they need to have state every single time and the component is used all over the place (or maybe then need a state array for several elements), so they move the state inside the component.
  3. Regret 🤣

My point is that we should move the logic to a custom hook so we can reuse it either partially or completely.

Now the actual reason for you to use this pattern isn't about reusability or data-flow as the article suggests, it's whether you need default props for your Counter component, for example, if you have this Counter in many places in your app which all have the same behavior.

That's another reason, but not "THE reason". As I mentioned, when I started using this pattern, I started getting all the benefits from stateless and stateful components.

Another thing, the second problem is trivial, if you need the parent component to know the child component's state then just lift the state up.

Not trivial at all. Once you move the state inside a component, you change the interface (public props) and the code of the component to have internal state.

// You go from:
<Counter
    count={count}
    onDecrement={decrementHandler}
    onIncrement={incrementHandler}
/>

// To
<Counter
    initialCount={0}
    step={1}
/>
Enter fullscreen mode Exit fullscreen mode

So "lifting" that state is not as trivial as you suggest because the change not only affects how the component behaves internally, but also how it interacts with the "external world". With the paired hook approach you never have to do that, you just write stateless components designed to not have any kind of internal state, and then write a hook to handle the state of that component in a custom hook that you can use if you need, but you don't have to. So you don't have "one or the other" of the two examples above, you get both and all in between.

Thanks for taking the time to read and comment!

Collapse
 
nasheomirro profile image
Nashe Omirro

your first code snippet has one of the examples in the article itself and one pretty common approach that from my point of view is "pretty bad" as well

I agree that just passing the value and the setter to the component could potentially be bad in some contexts since the component can do whatever it wants with the state, I should have emphasized that I was also against that, and would prefer passing functions like onIncrement down instead and only do the former in specific cases.

Yes, but not quite. The pattern is taking the logic particular to a component into a hook, but that state is usually moved by devs inside the component itself to avoid having to set it up every time in the parent, so is more like we are moving the state from the Child instead of from the Parent.

Thinking about it, it can be both from the child or the parent so yeah, I didn't think the pattern could be used for both stateful and stateless components, I was only thinking about stateless components initially.

That's another reason, but not "THE reason". As I mentioned, when I started using this pattern, I started getting all the benefits from stateless and stateful components.

Actually now putting stateful components into mind, since the pattern lets us detach the logic from the component and pass it in as props instead, we could modify the behavior by passing different functions, that's pretty powerful!!

The reason I came to the misunderstanding though is that you started of with a stateless component, and then moved to a stateful component as a solution to writing the logic every time, I thought the pattern was still about how to write stateless components so I missed a lot of the pattern's benefits. Thanks for clarifying!

I still can't understand the second problem's example where you want the parent to know the child's state though, I know you can do that but that just causes un-necessary renders and as you said, the state lives in two places, not to mention useEffect runs after everything is rendered so that causes some latency.

 
lukeshiru profile image
Luke Shiru • Edited

I still can't understand the second problem's example where you want the parent to know the child's state though, I know you can do that but that just causes un-necessary renders and as you said, the state lives in two places, not to mention useEffect runs after everything is rendered so that causes some latency.

The second problem is a pretty common one in which devs tend to "patch the patch" instead of fixing the root problem. Devs tend to move the state inside the component when they notice that they need it every single time, but then when they need to know the current state from the parent (for example, we want to show the total of all the Counter values we have on the screen), then they have solutions like using a useEffect to update the parent's state when the child state changes, so because we have two states representing the same thing, we need a side effect to keep them in sync (almost like one of those nasty double bindings). That's what I wanted to represent with this image:

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

This is obviously very bad. If we had the state only in the parent, then the data flow would be way simpler and predictable. Correct me if I'm wrong, but I think you took that part of the article as I was suggesting we should do that, but my idea was to show what people tends to do, and the point was to say we shouldn't do that, and to illustrate the problems the useEffect approach has.

 
nasheomirro profile image
Nashe Omirro • Edited

No I didn't think that you were suggesting that we should do that, I just found it weird that it was there and you had to explain that it's bad practice, I figured you could have instead said that the only way was to lift the state up again.

Honestly my bad on that one, I thought it was very obvious not to do that but I forgot that a lot of people with varying experiences and knowledge can read this, and that they might not know that information. Thanks again for the clarification!

 
lukeshiru profile image
Luke Shiru

Thanks to you for commenting! Folks that might have similar concerns to yours will get the clarifications on the comment section :D

Collapse
 
manssorr profile image
Mansour Koura

I'm excited to use this on my next project 😄