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.
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?
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
Again, if the computed valued is an async function, then an
useEffect is required.
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
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…
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…
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
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 is used in the
App component to define the value for the rest of the tree and in
Navigation component is none the wiser!
But what happens when you need to override a context value? Do we need to add
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
Consider another issue, in order to use this translate function, we would need to keep repeating ourselves with
useContext. We can do better.
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.
React component re-rendering 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 have changed.
React.memo is used to reduce the re-rendering by 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 the component are also checked for changes by
React.memo. Function equality is not possible aside from checking for identity.
React.memo uses triple equals
=== to check for changes for each prop and functions are never triple equals
=== unless they are the same function.
The solution is to use the
useCallback hook, but one needs to be careful about how to use it as the naïve approach can lead to returning a different function each time anyway. This would effectively fail to achieve the intention of avoiding re-renders with
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
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.