How to smoothly manage shared logic with custom React hooks

Paweł Puzio
Jit Team
Published in
6 min readNov 4, 2020

--

Photo by Vishal Jadhav on Unsplash

As our apps grow in complexity, there’s an inevitable increase in the component logic that follows along the way. We need to address aspects such as custom error handling, data transforming or applying domain specific business logic. We might usually deem it as straightforward, as long as we don’t have to deal with any side effects or internal state — we simply create a function that’s also easy to reuse. However, sooner or later, we’ll stumble upon a more advanced case.

Disclaimer — this article will only try to demonstrate why handling external logic with hooks might be more readable, easily testable (I’ll include examples of tests for hooks under the code examples) and scalable in the long run. I’m not aiming to belittle the previous API or idealize Hooks in any way.

Lots of React developers will remember working with the Component API that made use of JavaScript classes. For people coming from a more systematic background, such as backend languages or Angular, it faciliated the process of working with React. Yes, they still had to face the intricacies of working with e.g. the JSX syntax, but it reminded them of code that they were accustomed to working with, especially when paired with TypeScript (sadly, not supported out of the box by the Create-react-app CLI at that time).

Unfortunately, the Component API has its pitfalls, existence of which we realized mostly after the introduction of Hooks. Where should we begin? Local state of a component can only be pointing to one reference — with hooks we can create as many useState methods as we wish.
Yes, the fact that the in-built setState function would only overwrite the part of state that we want it to helps, but it still makes local state a bit cluttered at times. Lifecycle methods are a pain to control once you have several different, unrelated topics to handle and introduce a pretty steep learning curve as well.

And the worst pitfall? Higher-Order Components. Back then it was the only way to handle inserting logic into components and gave us some hope of being able to overcome it in a way that wouldn’t make us ill, but it comes with its own problems. Remember how Promises helped us avoid the famous callback hell when handling asynchronous operations? HOCs would often result in a HOC hell, where we’d wrap a component in a multitude of providers. This often resulted in not only worse readability but could also potentially mean hard to find bugs — for example in the case of a name collision.

Let’s say we wanted to create a reusable component that would make use of the React-router-dom library parameters. We’d have to first wrap it with the withRouter HOC, then create a new class extending React.Component, that handles the logic inside (possibly in some lifecycle hooks) and sends the parameters down via props to a child component. What’s the problem here? First, we’re adding two additional levels that could result in bugs — each of these HOC-s could potentially misbehave, so we’d have to analyze each of them if a problem were to occur. Secondly, we’re extending the props object, adding additional work with typing and potentially introducing performance issues (since props are one of the triggers for re-rendering components).

So, let’s see how such problems were overcome and simplified with Hooks. Let’s first look at an example of logic that’s set inside of a functional component.

Inside, we have a simple, one-line declaration of the state hook, as well as the useParams hook responsible for selecting a parameter from the URL. There’s also a single useEffect hook that will react according to the parameter change. Can you already see an improvement? We didn’t have to wrap the component with withRouter, because useParams is a (spoiler) custom hook responsible for fetching only the part of the object provided by withRouter we’re interested in — parameters — while with Components we’d also receive the history and location objects.
But what we’d like to reuse this logic across the application? That’s where the custom hooks magic begins.

This change might appear a bit confusing — most of the code is repeated. We created a function called useFetchBook, which includes all of the logic present above but exported to a separate file. As you might have already noticed, we’re still using the useParams custom hook as well as the basic useState hook inside of our new hook — yes, we can nest hooks, which means we don’t have to pass such parameters as arguments, a thing that was unimaginable with the Component API.
This results in much cleaner code and simpler debugging experience. But how do we use our custom hook in our component?

The useParams usage might have spoiled it — we just have to invoke the custom hook inside of our function component and destructure the properties we need. We can now reuse it all across the application in a very simple and comprehensible way, instead of having to resort to using some HOC mumbo-jumbo.

Let’s look at another example — a custom hook that will handle the frontend pagination logic for us. Inside, we’ll have two simple state hooks that will store the current page and amount of items per page, as well as methods that will handle changing the page, items per page and resetting the page. The code looks as follows.

t

One might ask why the methods are wrapped with useCallback — using it can improve performance in some use cases — more in this article that Oskar wrote. In our experience, when using this hook with tables, we noticed some unexpected performance issues before wrapping above functions with useCallback.

Now, we only have to create an instance of usePaginatedTable in our component and pass it to the table.

What if we wanted to make sure that our custom hook will always handle the pagination logic correctly? That’s where testing-library/react-hooks-testing-library comes in handy. Let’s see how we would test our newly created hook with the help of it.

We need to import two methods from the library in this example — the act method that will allow us to perform e.g. state changes, and the renderHook method that will generate the result of calling our hook. One thing that might be surprising is that the result of the rendering is stored in an object that looks like a ref. There’s also another thing that has to be mentioned — as you might have noticed, we don’t destructure the properties from our result. We do it for a reason — to quote the official documentation:

NOTE: There’s a gotcha with updates. renderHook mutates the value of current when updates happen so you cannot destructure its values as the assignment will make a copy locking into the value at that time.

While this already makes working with hooks that return an object pretty uncomfortable, it’s absolutely dreadful when a hook returns an array — and it’s not such a rare case. Imagine a hook which we know that might be invoked multiple times in a function — one example might be a hook responsible for handling the toggle mechanism.

Example of the useToggle hook

We want to return an array here for the simple reason of not having to always reassign the name returned by the hook — as you might already know, destructuring an array means that we can call the destructured elements any way we want. But what does that mean in the context of testing?

As you might have noticed, I wrote the order of elements in the array and their names in a comment. So far, that’s the best way of testing hooks which return an array that I’ve come up with.

To sum up: custom hooks are a great way to extract reusable logic to an easily testable function, while also allowing us to transform a humongous chunk of code into an elegant one-liner. Also, although the current standard library for testing them might not have the most elegant API and might be tricky at first, it’s still more straightforward than e.g. Enzyme.

--

--

Paweł Puzio
Jit Team

Software Engineer (React/Node/WASM) @ CodeComply.ai. I’m all about React, traveling, foreign languages, and photography.