Memoization in React and its myths.
Igor Snitkin
Posted on April 19, 2022
Hey kids, how are you? Today we will dive a bit deeper into React and hopefully, I will blow your mind about some of the misconceptions you have. Particularly, we'll be talking about rendering and re-rendering, how we can prevent components from being re-rendered, and whether or not to do it in the first place.
Before we start though, let's align on terminology, so we're on the same page throughout this article:
Mounting/unmounting
We use these terms to describe a moment when components are added to the DOM and subsequently drawn on the screen, or removed from the DOM and screen. It is always rather an expensive operation.
Rendering
The term "rendering", despite common beliefs, has nothing to do with rendering on the screen. Instead, it takes its name from the .render()
method of React Component class, basically meaning invocation of the method. In the modern world of functional components, rendering literally means calling your function component. This call will produce a new sub-tree and trigger reconciliation, also known as diffing to determine what has to be updated on the screen, if any at all. This operation is considerably less expensive compared to mounting and React team claims O(n)
time complexity where n
is the number of nodes within a subtree. The most important takeaway here is that re-rendering will not cause re-mounting.
Lifecycle effects
The main and the most important difference between props and state is that props will be updated on rendering and state on mounting, persisting between re-rendering stages. This means that every time state is dependent on props, lifecycle effect has to be introduced:
const ArticleList = ({ topics }) => {
// This will be initialized on mount only
const [articles, setArticles] = React.useState([]);
// Update list of articles depending on `topics` prop
React.useEffect(() => {
fetchArticles(topics)
.then(data => {
setArticles(data);
});
}, [topics]);
};
Primitive/non-primitive values
Primitive values in React are, well, the primitive values in JavaScript. If you're not sure what are those, this article might be too hardcore for you buddy. Non-primitive are the rest: functions, objects, arrays you name it. We can have primitive/non-primitive props and primitive/non-primitive stateful values.
As a rule of thumb and if there's a choice you should always prefer primitive props to non-primitive props:
// OK
const Address = ({ addressObj }) => {
/** Display address */
};
// Better
const Address = ({
streetLine1,
streetLine2,
locality,
postalCode,
country,
}) => {
/** Display address */
};
"Wait, what?" I literally hear your brain screaming at me right now. Explaining this will derail us from the scope of this article, so let's just say there are certain optimizations already in place around primitive values, and the best optimization is eliminating the need to be optimized in the first place.
Still not convinced? Ok, consider the two components below and try to guess which one will blow up your call stack (hint: there is only one):
const Primitive = () => {
const [bool, setBool] = React.useState(false);
// Now, let's have some fun!
React.useEffect(() => {
setBool(false);
});
};
const NonPrimitive = () => {
const [arr, setArr] = React.useState([]);
// Now, let's have even more fun!
React.useEffect(() => {
setArr([]);
});
};
Dependency arrays
Most React hooks will take a callback as the first argument and a dependency array as the second, so the callback is called only if any of the values from the dependency array change. Without any exception, every prop, or any value derived from the prop, used in the callback has to appear in the dependency array. Again, this article is not about dependency arrays and I won't bother explaining why, but I'm pretty sure you can find a good explanation of why dependencies have to be exhausted online. I strongly advise using react-hooks/exhaustive-deps
ESLint rule to guard against this rule.
Myths
Cool! Let's start with some of the myths majority of React developers believe in and let's see if you're one of them!
Inline handlers myth
This is super known and the same time super dumb one. It goes something like: You should not inline event handler not to cause extra re-renders:
const handleClick = (e) => { /** handle click */ };
return (
<>
{/** BAD */}
<Button onClick={(e) => { /** handle click */ }} />
{/** GOOD */}
<Button onClick={handleClick} />
</>
);
Of course, this is total BS! The fact that you assign a new function to a variable before passing it as a prop changes absolutely nothing. Not only Button
component will re-render, but also a new instance of the function will be passed on every render:
Myth #2: Memoization hooks will prevent components from being re-rendered
So the solution is easy - just wrap your handler in useCallback
or useMemo
hook, right? Wrong! Even though you will be passing the same memoized instance of the function, it will not prevent the component from being re-rendered. In fact, nothing will stop your regular function component from being re-rendered in the case when the parent is being re-rendered:
Memoization in React
As we just determined, memoization hooks are almost pointless (more about them a bit later), so how do we fix this re-rendering issue. Meet React.memo
higher-order component aimed to memoize the component itself and not re-render it in case the same props are provided. So basically, you can trade prop comparison, which is even more performant, to a subtree diffing.
Why is it more performant you can ask. Because a number of props will be less on average compared to a number of nodes in a sub-tree, and because React by default, will use shallow comparison which is an extremely lightweight operation. All you need to do is wrap your component in React.memo
:
const Button = React.memo(({ label, handler }) => (
<button type="button" onClick={handler}>
{label}
</button>
));
It's important to understand that React.memo
is not a silver bullet and will not save you from re-rendering if different props are passed:
In other words, if you started playing a memoization game you will have to fully commit to it memoizing absolutely every non-primitive prop, otherwise not only it will be pointless, but your app will be less optimized as it will perform both prop comparison and diffing on each re-render.
The same principle abstracts down to React.useMemo
hook - every non-primitive value the calculation is dependent on has to be memoized, otherwise your code will actually perform worse:
const Page = () => {
const { data: users } = useUsers();
const filteredUsers = users?.filter(filterFn);
return (
<>
{filteredUsers && <RoleList users={filteredUsers} />}
</>
);
};
const RoleList = ({ users }) => {
// Every time new users list provided, group them by role
const roles = React.useMemo(() => groupBy(users, 'role'), [users]);
};
In the code above, the fact that users
from useUsers
hook is a stateful value (that persists during re-rendering stages) might give you a false assumption that filteredUsers
also will persist, whereas in reality a completely new instance of the array will be created on each render totally obliterating your memoization efforts in RoleList
component and making it, in fact, less performant.
Summary
All right, so when should you use memoization in React? That's a good question and, as you might have guessed by now, the answer isn't that simple. But let's summarize a couple of rules that might help you decide:
- You should memoize every non-primitive prop causing non-primitive state effects
- Generally, any non-primitive value appearing in dependency arrays of React hooks has to be memoized.
- You should avoid, if possible, non-primitive props and do not use the default parameters for optional non-primitive props. This will eliminate the need for memoization.
- If the parent component has a large number of children, think of the list of items or rows of the table, and each such child updates parent's state - this is a perfect recipe for
React.memo
usage. In this case all non-primitive props have to be memoized.
Have fun, listen to music, and good luck!
Posted on April 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 5, 2024