Visualizing React Memoization

One of the most useful features in React is memoization. We can use memoization to prevent React from re-rendering expensive parts of our react subtree.This article will visualize how state changes propagates down the React component tree then apply me…


This content originally appeared on Level Up Coding - Medium and was authored by Yasser El-Sayed

One of the most useful features in React is memoization. We can use memoization to prevent React from re-rendering expensive parts of our react subtree.

This article will visualize how state changes propagates down the React component tree then apply memo and see how that propagation changes.

This was a fun experiment so I hope you enjoy and learn something new!

Note: all the code for this experiment can be found here

Default Behavior

By default when a parent component’s state changes using setState or a change in props React re-renders all the children of that entire subtree. This is because React cannot be sure that a change in the parent’s state would affect the rendering of the child components. Some components are not “Pure” in that there are some side effects that happen because of change of a parent state. This is the case even if the data change happens for only ONE child component, as I’ll present in the following visualization.

First of, let’s get our experiment playground set up. We will render a list of 9 components each presenting personal data for one person:

Simple list of components to experiment with

I kept the code pretty simple:

import {useState} from "react";
import './App.css';
import {NameCard} from './components/NameCard';
import {generateDataList} from "./utils";
let masterData = generateDataList(9);
// Shows Personal Data
export const NameCard = memo(({ id, name, sex, age }) => {
const cardId = `name-card-${id}`;

return (
<div className="name-card" id={cardId}>
<div className="name-card__title">
<h2 className="name-card__title__id"> {id}. </h2>
<h3 className="name-card__title__name"> {name} </h3>
</div>
<p> Sex: {sex} </p>
<p> Age: {age} </p>
</div>
);
});
function App() {
const [data, setData] = useState(masterData);
    return (
<div className="App">
<div className="name-list-container">
{/* Render each one of the names */}
{data.map(i =>
<NameCard
{/* Important Note: we're using key here... */}
key={i.id}
name={i.name}
id={i.id}
sex={i.sex}
age={i.age}
/>
)}
</div>
</div>
);
}
export default App;

In the code above we have the App component that renders a simplelist of NameCard components.

Re-ordering the Components

It’s important to first understand the expected behavior. Since we’re using the key prop with a unique ID for each component, if the position of one of those component changes then the NameCard components should NOT be re-rendered. The position of the components in the parent tree should only change, but React knows the components themselves have not changed (since there was no change in the passed props or internal state of each component) i.e. the reconciliation process should not re-render those components but simply re-order them.

Let’s test that!

First let’s build a way for us to shuffle the list.

// Simple shuffle function, details not important
function shuffle(array) {
let currentIndex = array.length - 1, randomIndex;
const newArray = [...array];
    // While there remain elements to shuffle.
while (currentIndex !== 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
        // And swap it with the current element.
[newArray[currentIndex], newArray[randomIndex]] = [newArray[randomIndex], newArray[currentIndex]];
        currentIndex--;
}
    return newArray;
}
function App() {
const [data, setData] = useState(masterData);
    // We added a handler to the shuffle button `onClick`
const onShuffle = () => setData((oldData) => shuffle(oldData));
    return (
<div className="App">
{/* New Button added here */}
<button onClick={onShuffle}>
Shuffle Elements
</button>
<div className="name-list-container">
{data.map(i =>
<NameCard
key={i.id}
name={i.name}
id={i.id}
sex={i.sex}
age={i.age}
/>
)}
</div>
</div>
);
}

Second, let’s modify the NameCard component so that we can tell whether the component has re-rendered. We can do this by adding a simple background fade animation.

// Will add a class that has a `keyframe` animation then remove it
const addAnimation = (cardId) => {
// Would have been better to pass in a `ref` but I got lazy lol
const card = document.getElementById(cardId);
if (!card) return;
    card.classList.add("name-card__fade");
card.addEventListener("animationend", () => {
card.classList.remove("name-card__fade");
});
}
const NameCard = ({ id, name, sex, age }) => {
const cardId = `name-card-${id}`;

// Every time this component is mounted or updated, the animation will be added.
useEffect(() => {
addAnimation(cardId);
});

return (
<div className="name-card" id={cardId}>
<div className="name-card__title">
<h2 className="name-card__title__id"> {id}. </h2>
<h3 className="name-card__title__name"> {name} </h3>
</div>
<p> Sex: {sex} </p>
<p> Age: {age} </p>
</div>
);
};

Let’s try it!

All the components are re-rendering on shuffle

Good news is that the animation is working. The bad news is that when I click shuffle all the components re-render, despite the fact that we’re using the key prop. You can see that the animation plays on all the components when I click on the “Shuffle” button.

You might be thinking that the data of the whole array changes since the shuffle assigns a new list, so before we resolve this, it would be interesting to see what would happen if we just change only one element in the array.

One Element Change

Much like the previous experiment, we’ll add one more button that changes the name of one the persons.

import {faker} from "@faker-js/faker";
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
function App() {
const [data, setData] = useState(masterData);
    const onShuffle = () => setData((oldData) => shuffle(oldData));

// Added a function that will pick a random person and change their name
const onChange = () => {
const randomIndex = getRandomInt(data.length - 1);
const randomPerson = data[randomIndex];
        // Faker will provide us with a new name at random
randomPerson.name = faker.person.fullName(randomPerson.sex);
setData([...data]);
};
    return (
<div className="App">
<button onClick={onShuffle}>
Shuffle Elements
</button>
<div className="name-list-container">
{data.map(i =>
<NameCard
key={i.id}
name={i.name}
id={i.id}
sex={i.sex}
age={i.age}
/>
)}
</div>
</div>
);
}

Let’s try it!

All the components are re-rendering even though only one person’s name is changing

Again we see the same result. Although we can tell that only one name is changing in the gif above, all the components are re-rendered. Ideally this should not be happening, and it can be costly in production environment where some routes/components are computationally/memory intensive.

In the next section we will utilize the React memoization in order to resolve this.

Memoization

React provides a pretty easy way for us to do memoization and prevent the whole parent subtree from re-rendering. All we have to do is wrap our components with a memo function provided by React.

import {memo} from "react";
// Just add the `memo` here, it's like magic
export const NameCard = memo(({ id, name, sex, age }) => {
// Same old code from before...
});

Now let’s re-try both experiments from above! We’ll start with the random name change.

Only one component is re-rendering at a time.

Much better! Only the components that need to re-render are doing so. Let’s try the shuffle one more time.

No components re-rendering

Even better! Since the components are being re-ordered on the parent subtree, there is no need for any components to re-render.

Child Subtree

An interesting experiment would be to try the same thing with the subtree of the child component as well. I added another data point for “Favorite Colors” which will render a list of components called ColorName.

Same as before but with the additional “Favorite Colors”

I will run the same visualization by adding the same animation to the ColorName as the NameCard component; whenever the data changes we should see it pop out. Here is the code for that:

const ColorName = ({ id, name }) => {
const colorId = `color-id-${id}`;

// This is the same as the `NameCard` component, we should see it pop out if it re-renders
useEffect(() => {
addAnimation(colorId);
});

return (
<span id={colorId} className="child-name"> {name} </span>
);
}
const NameCard = memo(({ id, name, sex, age, colors }) => {
const cardId = `name-card-${id}`;

useEffect(() => {
addAnimation(cardId);
});

return (
<div className="name-card" id={cardId}>
<div className="name-card__title">
<h2 className="name-card__title__id"> {id}. </h2>
<h3 className="name-card__title__name"> {name} </h3>
</div>
<p> Sex: {sex} </p>
<p> Age: {age} </p>
{colors && (
<p> Favorite Colors: {colors.map(color =>
<ColorName
{/* Not the best way to do this but you get the idea */}
key={`${cardId}${color.id}`}
id={`${cardId}${color.id}`}
name={color.name}
/>)}
</p>
)}
</div>
);
});

Now let’s see what happens.

The color components are also rendering

Let’s change the component code to use memo and run it again.

const ColorName = memo(({ id, name }) => {
const colorId = `color-id-${id}`;

// This is the same as the `NameCard` component, we should see it pop out if it re-renders
useEffect(() => {
addAnimation(colorId);
});

return (
<span id={colorId} className="child-name"> {name} </span>
);
})
The color components are no longer rendering

As we can see memo can also stop the propagation in the child subtrees as well. This seems obvious in hind sight but still interesting to visualize.

Pure Components

If you’re familiar with React Pure Components you might have found yourself wondering what’s the difference between these memoized components and Pure Components? The answer is nothing: memoized components are Pure Components.

Pure Components are components that give the same output for the same state and props. Meaning that they only re-render if the props or state changes. Memoization as explained above is simply the functional way of achieving Pure Components.

Memoize Everything?

The visual representation of this helps us understand that React by default does not memoize components by default. In certain cases this may cause some performance issues especially if your app is frequently updating a global state. React does not know if updating the state of the parent will affect the children because some components can contain side effects. Thus, memoizing everything by default can potentially cause unwanted behavior that’s difficult to debug.

As the developer, however, you DO know the specifics of your components. I personally do not recommend memoizing all your components as memoization has a memory cost to your application, just like using useMemo or useCallback when not needed can cause performance issues over time. React is pretty efficient and the speed benefits you get from memoization may be marginal. You basically have to ask yourself three questions:

  1. Is this component “Pure” (i.e. it does not contain any side effects)?
  2. Is this component frequently updated, or is it’s parent frequently changing its state?
  3. Is this component computationally/memory intensive?

If the answer to all these three is yes then I think it’s worthwhile. In the end of the day this is an optimization. You need to apply it as you see fit and not overuse it.

I hope this visualization was helpful to shed light on how component subtrees are rendered and what you can do to optimize it.

Thanks for reading!


Visualizing React Memoization was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Yasser El-Sayed


Print Share Comment Cite Upload Translate Updates
APA

Yasser El-Sayed | Sciencx (2024-09-19T21:02:20+00:00) Visualizing React Memoization. Retrieved from https://www.scien.cx/2024/09/19/visualizing-react-memoization/

MLA
" » Visualizing React Memoization." Yasser El-Sayed | Sciencx - Thursday September 19, 2024, https://www.scien.cx/2024/09/19/visualizing-react-memoization/
HARVARD
Yasser El-Sayed | Sciencx Thursday September 19, 2024 » Visualizing React Memoization., viewed ,<https://www.scien.cx/2024/09/19/visualizing-react-memoization/>
VANCOUVER
Yasser El-Sayed | Sciencx - » Visualizing React Memoization. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/09/19/visualizing-react-memoization/
CHICAGO
" » Visualizing React Memoization." Yasser El-Sayed | Sciencx - Accessed . https://www.scien.cx/2024/09/19/visualizing-react-memoization/
IEEE
" » Visualizing React Memoization." Yasser El-Sayed | Sciencx [Online]. Available: https://www.scien.cx/2024/09/19/visualizing-react-memoization/. [Accessed: ]
rf:citation
» Visualizing React Memoization | Yasser El-Sayed | Sciencx | https://www.scien.cx/2024/09/19/visualizing-react-memoization/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.