The paired hook pattern – DEV Community

After years of working with React and TypeScript, I’ve seen lots of patterns for part growth, however up to now I did not see one which works pretty much as good for perform elements because the “paired hook sample”. To get began let’s use a basic: The Counter part.



A easy instance

First we write a stateless part:

const Counter = ({ rely, onDecrement, onIncrement }) => (
    <>
        <span>{rely}</span>
        <button onClick={onIncrement}>+</button>
        <button onClick={onDecrement}>-</button>
    </>
);
Enter fullscreen mode

Exit fullscreen mode

And after we use it, we have to create a state for it:

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

    return (
        <Counter
            rely={rely}
            onDecrement={() => setCount(rely - 1)}
            onIncrement={() => setCount(rely + 1)}
        />
    );
};
Enter fullscreen mode

Exit fullscreen mode

The dynamic seems to be one thing like this:



The primary downside: Reuse

The issue with the stateless part is that we have to use the useState hook each time we use the part, which is perhaps annoying for elements that require extra properties and are throughout your app.

So, is fairly widespread to simply put the state instantly within the part. Doing this we need not have a state each time we use it, so then our Counter part adjustments to one thing like this:

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

    return (
        <>
            <span>{rely}</span>
            <button onClick={() => setCount(rely + step)}>+</button>
            <button onClick={() => setCount(rely - step)}>-</button>
        </>
    );
};
Enter fullscreen mode

Exit fullscreen mode

After which to make use of it, as many occasions as we wish with out having to create a state for every:

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

Exit fullscreen mode

The dynamic then seems to be like this:

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



The second downside: Information circulation

Now, that is nice till we need to know the present state of the counter component from the mum or dad component. So that you is perhaps tempted to create a monster like this one:

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

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

    return (
        <>
            <span>{rely}</span>
            <button onClick={() => setCount(rely + step)}>+</button>
            <button onClick={() => setCount(rely - step)}>-</button>
        </>
    );
};
Enter fullscreen mode

Exit fullscreen mode

After which use it like this:

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

    return (
        <>
            <span>Present rely in Counter: {rely}</span>
            <Counter onCountChange={setCount} />
        </>
    );
};
Enter fullscreen mode

Exit fullscreen mode

It may not be apparent at first, however we’re introducing unintended effects to each state change simply to maintain the mum or dad in sync with the youngsters, and this has two important points:

  1. The state resides in two locations without delay (the mum or dad component and the youngsters).
  2. The youngsters are updating the state of the mum or dad, so we’re successfully going in opposition to the one-way knowledge circulation.

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



The paired hook sample

Top-of-the-line issues about hooks is after we create our personal. The answer I suggest for this subject is sort of easy, however I truthfully imagine solves the overwhelming majority of points with state I’ve seen round. Step one is just like what we had at the start right here, we simply create a stateless part:

const Counter = ({ rely, onDecrement, onIncrement }) => (
    <>
        <span>{rely}</span>
        <button onClick={onIncrement}>+</button>
        <button onClick={onDecrement}>-</button>
    </>
);
Enter fullscreen mode

Exit fullscreen mode

However this time, as a substitute of requiring the shoppers of our part to determine the state themselves, we create a hook that goes along with our part, we are able to name it useCounter. The primary requirement for this hook is that it must return an object with properties matching the properties of Counter:

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

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

Exit fullscreen mode

What this permits is that now we are able to use it nearly as a stateful part:

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

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

Exit fullscreen mode

But in addition we are able to use it as a stateless part:

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

Exit fullscreen mode

And we now not have limitations accessing the state, as a result of the state is definitely within the mum or dad.

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

    return (
        <>
            <span>Present rely in Counter: {rely}</span>
            <Counter {...{ rely, ...counterProps }} />
        </>
    );
};
Enter fullscreen mode

Exit fullscreen mode

The dynamic then seems to be one thing like this:

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

With this strategy, we’re actually making our part reusable by not making it require a context or bizarre callbacks primarily based on unintended effects or something like that. We simply have a pleasant pure stateless part, with a hook that we are able to move instantly or simply partially if we need to take management of any property particularly.

The title “paired hook” then comes from offering a hook with a stateless part that may be paired to it.



An issue (and answer) with the paired sample

The primary subject the paired hook strategy has is that now we’d like a hook for each part with some sort of state, which is ok when we have now a single part, however turns into tough when we have now a number of elements of the identical kind (like for instance having an inventory of Counter elements).

You is perhaps tempted to do one thing like this:

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

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

Exit fullscreen mode

However the issue with this strategy is that you are going in opposition to the rules of hooks since you’re calling the useCounter hook inside a loop. Now, if you consider it, you possibly can loop over elements which have their very own state, so one viable answer is to create a “paired” model of your part, which calls the hook for you:

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

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

// After which...
const App = ({ record }) => (
    <>
        {record.map(initialCount => (
            <PairedCounter initialCount={initialCount} />
        ))}
    </>
);
Enter fullscreen mode

Exit fullscreen mode

This strategy appears just like the stateful strategy (the second instance on this article) however is far more versatile and testable. The opposite strategy we have now is to create a part context for each merchandise with out having to write down a part ourselves, and for that, I created a small perform that I printed in npm referred to as react-pair:

React Pair logo

The perform is so easy, you can write it your self, the one distinction is that I am testing it, including devtools integration, and typing with TypeScript for you. You may examine the supply here. The utilization is sort of easy, react-pair supplies a pair perform that you need to use to create a part that offers you entry to the hook in a part context (with out breaking the principles of hooks):

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

const PairedCounter = pair(useCounter);

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

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

Exit fullscreen mode

Simply to be clear, you need not use react-pair to realize this, you possibly can simply create a brand new stateful part by hand, that simply pairs the hook with the part.

Both if you happen to use the util or not, the ensuing dynamic seems to be one thing like this:

Diagram showing several component+hook pairs at the same level



TL;DR

  • Write a stateless part, designed to work in isolation.
  • Write a customized hook to be paired with that part.
  • Use the part with the hook for a stateful expertise.
  • Use the part with out the hook for a stateless expertise.
  • Use the part with only a few properties from the hook for a blended expertise.
  • Use an util or a wrapper part when looping.



Closing ideas

I have been utilizing this sample for some time now and up to now I did not discovered any blocking points with it, so I invite you to strive it out in certainly one of your initiatives and inform me the way it goes!

Add a Comment

Your email address will not be published. Required fields are marked *