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 useMemo
.
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 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
?
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.
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.
In English, 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 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
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.