How to escape React Hooks Hell

Ronald Chen
Battlefy
Published in
5 min readOct 5, 2021

--

When learning React Hooks, it seems like black magic. How the heck does useState retains its value? Over time, some learn the hook incantations to get stuff done. Even fewer eventually understand how hooks actually work.

But before then, it’s so easy to make a mess with React Hooks. In this article I will be showing some common issues as people start to really use React Hooks.

Unnecessary useEffect to initialize setState

One of the first incantations people learn is using useEffect to run some initialization code once per mount. This leads people to abuse useEffect to set the initial value for useState. But the useEffect is redundant in this case. useState can take an initial value or function.

Note, if the initial value comes from an async function, then there is no choice but to use useEffect. But what if the initial value is costly and depends on a prop value?

Unnecessary useEffect and useState for computed value

Sometimes there is a costly function that needs to be computed based off of a prop value. The naïve solution is to trigger the computation with useEffect with the prop value as the dependency and store the result with useState, but this can be simplified down to a single useMemo.

Again, if the computed valued is an async function, then an useEffect is required.

Never use objects or arrays as dependencies

One of the most mystifying concepts with React Hooks is the dependency list. Why does it matter? When objects or arrays used as dependencies, it seems to just work, but this actually introduced a performance bug. React only uses triple equals === to check for dependency changes, thus it is not suitable for objects nor arrays.

When objects or arrays are used as dependencies, it effectively always re-runs the hook.

See this in action with a full demo: https://stackblitz.com/edit/react-5xghb4

Don’t useMemo entire inner components

Once people understand how useMemo can be used to improve performance, it becomes easy to be used poorly. Sometimes the initialization logic for components become expensive, and it’s tempting to wrap the whole declaration of the component with useMemo, but the correct thing is to only wrap useMemo around the costly part.

But wait, isn’t this a contrived example since SlowComponent is an inner component? Well yes, which leads to…

Avoid inner components

Inner components are often introduced as a means to avoid passing props to immediate children. They do have their place when the parent/child component is very tightly coupled and the child component doesn’t make sense on its own.

As that may be, don’t use an inner component due to pure laziness of passing props. Extracting inner components makes it easier to test and reuse. It makes it easier to read the code of the parent component without having to keep in mind which variable are leaking in scope into the inner component.

But what if you need to pass a lot of props? One might run into prop drilling…

useContext instead of prop drilling

In large React apps, the component tree becomes tall. Often there are props that need to be passed down through several levels in the component tree and it becomes a bunch of busy work as the intermediate levels just pass the prop along. This is called prop drilling and best way to avoid this is to flatten the component tree using props.children.

However that isn’t always possible or desired. Another solution is useContext. This is commonly used handle “global” options like locale, theme, logged in user, etc.

In this example, we have 3 components, where the Link component requires the locale to get a translated version of the text, but there is an intermediate Navigation component. Here we see the Navigation component just passes the locale along. This is a short example, but in the real world, there can be many more intermediate levels.

We solve this problem by creating a LocaleContext. The LocaleContext is used in the App component to define the value for the rest of the tree and in Link with useContext. Notice Navigation component is none the wiser!

But what happens when you need to override a context value? Do we need to add OverrideLocaleContext?

Override context are unnecessary

It may seem like a context can only hold one value, but really it’s just a reference. The real value is held in the Provider. Multiple providers can be nested and useContext will return the value to the closest Provider.

Consider another issue, in order to use this translate function, we would need to keep repeating ourselves with useContext. We can do better.

Extract repeated hook usage into a custom hook

There is no one way to organize contexts, but here is how I usually do it. I’ll also show you show I would convert the usage of translate into a custom hook that is also performant.

Avoid re-renders by reducing useCallback dependencies

When a React component re-render is an advanced topic and I won’t do it full justice here. But I will try to illustrate a small example of it. By default React will re-render the entire component tree and this leads to problems on very large sites. One optimization is to only re-render parts of the component tree that has changed.

React.memo is used to reduce the re-renders enhancing components to only re-render if any of the props have changed. When one tries to use React.memo they will quickly wonder why it doesn’t seem to work. For non-trivial components, callback handlers such as onClick are often used. The function props passed into component are also checked for changes by React.memo. Function equality is not possible asides from checking for identity.

In English, React.memo uses triple equals === to check for changes for each prop and functions are never triple equals === unless its the same function.

The solution is to use the useCallback hook, but one needs to be careful on how to use it as the naïve approach can lead to returning a different function each time anyways. This would effectively fail to achieve the intention of avoid re-renders with React.memo.

One way to apply useCallback correctly is to remove the dependency that was causing the new function to be returned. This is becoming unintelligible, but is easier to understand with a code example.

Run this for yourself in a full demo: https://stackblitz.com/edit/react-1r1rmg

The long game

I hope some of these patterns have helped you escape your own mini React Hooks Hell, but keep in mind there are far more pitfalls than I could ever possibly describe. The long game is to understand how the React Hook and render algorithm actually works.

Do you want to discover more React anti-patterns and their solutions? You’re in luck, Battlefy is hiring.

--

--