Optimizing React applications is essential for performance, and mastering memoization techniques can significantly improve efficiency. This article explores the rendering process in React, the reasons behind re-rendering, and how to use memoization methods like useMemo
, useCallback
, and memo
to optimize your components.
How does rendering work in React?
React is a component-driven framework that utilizes a top-down approach for rendering. When creating a new React app using Vite or a framework like Next.js, the first component to be rendered is typically the App
component (or the _app
file in Next.js). From there, React proceeds down the component tree, rendering each child of the parent component. This process continues until all components have been rendered.
The next question that comes to mind then is...
When does a component re-render?
The most common understanding is that "A component re-renders when it's props or state changes".
This is mostly correct. There is another important reason why a component re-renders: When its parent re-renders.
When a parent component re-renders, React's rendering mechanism ensures that the entire component tree within this parent component is also re-rendered. Let's examine an example to gain a better understanding.
Let us create two components:
A parent component with a count state with a button that increments the count on click.
A child component that randomly sorts an array of movies and displays them as a list.
// App.jsx
export default function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+1</button>
<Movies />
</div>
);
}
// Movies.tsx
const MOVIES = [
"Harry Potter and the Goblet of Fire",
"Titanic",
"Avatar",
"Terminator",
"Star Wars",
"The Lord of the Rings: The Return of the King"
];
const randomizeMovies = () => {
const newMovies = [...MOVIES];
for (let i = newMovies.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newMovies[i], newMovies[j]] = [newMovies[j], newMovies[i]];
}
return newMovies;
};
const Movies = () => {
return (
<ul>
{randomizeMovies().map((movie) => (
<li key={movie}>{movie}</li>
))}
</ul>
);
};
export default Movies;
Let's run this and see what happens.
The Movies component does not have any state or props being passed down, yet it still re-renders every time the count changes.
Why should we memoize in React?
We have a small array with only strings, so randomizing it during each render doesn't cause noticeable performance problems. However, if we had a large array with thousands of complex elements, it could lead to issues.
You might think this isn't a common situation, as usually large datasets are sorted on the backend and we just display them on the frontend. But imagine rendering a row with tags, inputs, buttons, and more for each movie. This could cause performance issues if you're showing 20-30 of them on a page.
How to optimize your components in React?
Now that we've seen how unnecessary renders might creep into our application, let us now look at how we can fix this with memoization.
What is memoization?
Wikipedia defines memoization as:
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.
What this means is that we call the expensive function only when the inputs to the function change.
In React, memoization can be accomplished using three methods:
useMemo
hookuseCallback
hookmemo
function
Let's look at each of them in detail.
useMemo
hook
useMemo
is a hook provided by React that accepts two arguments:
The first argument is a function that usually returns a value that is stored in a variable
A dependency array similar to what's used in
useEffect
Using this hook, we can make sure that the function passed as the first argument is only run when something specified in the dependency array changes.
Similar to the dependency array used in useEffect
, if we pass an empty array, useMemo
hook runs only once.
Let us now see how we can fix the problem we introduced in the first section using the useMemo
hook. Let us modify the Movies
component like so:
const Movies = () => {
const movies = useMemo(randomizeMovies, []);
return (
<ul>
{movies.map((movie) => (
<li key={movie}>{movie}</li>
))}
</ul>
);
};
Let us look at the result:
As you can see, randomization occurs only once when the component first renders. Hence, the Movies
component renders only once.
useCallback
hook
To understand the usage of this hook, we first need to understand the difference between primitive and reference data types.
Primitive vs Reference Data Types
Simple types store values directly in memory and the variable name is linked to the value. When these values are used in a function, they are copied. So, changes inside the function don't impact the original variable.
Examples of primitive data types in JavaScript are:
Numbers
String
Boolean
Null
Undefined
Imagine it as putting something inside a box and labelling it.
const a = 50;
Reference data types work differently. When you make one, the value goes into a place called heap memory, and the variable name points to that value.
const a = [1, 2, 3, 4]
When a reference type is passed to a function as an argument, only the pointer is copied into the function parameter. Therefore, any changes made to the parameter data within the function are also reflected in the variable outside the function.
Examples of reference data types in JavaScript are:
Objects
Functions
Arrays
Now, let's get back to the useCallback
hook.
The useCallback
hook - useMemo
for functions
The useCallback hook is like useMemo, but for functions. While useMemo remembers values returned from a function, useCallback remembers the whole function.
Why should we memoize functions?
As we observed, functions are reference data types. Therefore, when a function (defined within the parent component) is passed as a prop to a child component, only a pointer to that function is passed along. Consequently, even if the child component is memoized, each time the parent re-renders, the function is recreated, the pointer to the function changes, and as a result, the child component is re-rendered.
Let's look at an example.
Let's take our previous example and imagine that the randomize
function is defined in the App
component and is passed as a prop to the Movies
component.
// App.jsx
const MOVIES = [
"Harry Potter and the Goblet of Fire",
"Titanic",
"Avatar",
"Terminator",
"Star Wars",
"The Lord of the Rings: The Return of the King"
];
export default function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
const randomizeMovies = () => {
const newMovies = [...MOVIES];
for (let i = newMovies.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newMovies[i], newMovies[j]] = [newMovies[j], newMovies[i]];
}
return newMovies;
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+1</button>
<Movies randomizeMovies={randomizeMovies} />
</div>
);
}
const Movies = ({ randomizeMovies }) => {
const movies = useMemo(() => randomizeMovies(), [randomizeMovies]);
return (
<ul>
{movies.map((movie) => (
<li key={movie}>{movie}</li>
))}
</ul>
);
};
export default Movies;
Let's look at the result.
As you can see, even though we are memoizing the result of the randomize function, the Movies
component re-renders every time the parent App
renders. This is because the randomizeMovies
function changes with each render, and thus, the props of the Movies
component also changes. This causes useMemo
to run, and as a result, we see a different random order of movies every time App
re-renders.
We can fix this problem by using the useCallback
hook. Let's update the randomizeMovies
function like so:
const randomizeMovies = useCallback(() => {
const newMovies = [...MOVIES];
for (let i = newMovies.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newMovies[i], newMovies[j]] = [newMovies[j], newMovies[i]];
}
return newMovies;
}, []);
Let's look at the result now:
Now, it is the same as when we used useMemo
hook directly. The randomizeMovies
function is now only created when the component first renders since the dependency array in the useCallback
hook is empty.
Real-life example
A common real-life example of useCallback is in a search feature where we need to slow down or limit API calls so that the API request doesn't happen with every keypress.You can read more about it here: Debouncing in React.
Now let us look at another memoization technique in React - the memo
function.
memo
function
The memo
function can be used to memoize a whole component by passing the component as an argument and using the memoized component instead of using the component directly.
const ExampleComponent = ({ text }) => {
return (
<p>{text}</p>
)
}
export default React.memo(ExampleComponent)
What does the memo
function do?
The memo function ensures that the component only re-renders when the props change. Thus, the ExampleComponent
above only re-renders when text
changes.
Now, you must be thinking, why do we need useMemo
and useCallback
when we can just wrap every component in memo
?
There are two things to consider as an answer:
It is NOT a good idea to wrap every component in your app in
memo
. We will discuss more about this in the "When should we memoize in React?" section below.Consider our example in
useCallback
above. If we did not wraprandomizeMovies
inuseCallback
and wrappedMovies
inmemo
, do you thinkMovies
will not re-render every timecount
changes?
As you can see, Movies
renders every time count
changes. Why?
As mentioned earlier, memo
ensures that Movies
only renders when the props change and here, randomizeMovies
changes every time App
re-renders. Thus, the props of the Movies
component change every time count
changes and hence, memo
re-renders the Movies
component.
Now that we have taken a look at all three techniques in React used for the memoization of components, let us now see when to use what.
When should we memoize in React?
Let me tell you the golden rule for deciding when to memoize in React:
Use it only as a last resort
I understand, I understand! After learning so much about memoization and all its aspects, I am now advising you not to use it unless there is no other option.
The reason is very simple. If you go back and read the definition of memoization once more, you would notice that memoization requires caching of results based on inputs.
Storing the result in the cache also takes some time, although very little. If you memoize everything everywhere, this caching time will compound and your first render is affected - it becomes slow.
Thus, use memoization carefully. More often than not, there are better ways to solve a problem. Let us take our example problem above and show how it can be solved without using memoization at all.
All we need to do is separate the count logic into a separate component.
// App.jsx
export default function App() {
return (
<div className="App">
<Count />
<Movies />
</div>
);
}
// Count.jsx
const Count = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<>
<p>Count: {count}</p>
<button onClick={handleClick}>+1</button>
</>
);
};
export default Count;
const MOVIES = [
"Harry Potter and the Goblet of Fire",
"Titanic",
"Avatar",
"Terminator",
"Star Wars",
"The Lord of the Rings: The Return of the King"
];
const randomizeMovies = () => {
const newMovies = [...MOVIES];
for (let i = newMovies.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newMovies[i], newMovies[j]] = [newMovies[j], newMovies[i]];
}
return newMovies;
};
const Movies = () => {
return (
<ul>
{randomizeMovies().map((movie) => (
<li key={movie}>{movie}</li>
))}
</ul>
);
};
export default Movies;
Here is the result:
As you can see, just by making components wisely and storing the UI state as close to the component as possible, we can optimize our React components without even needing to use memoization.
Here are some more great articles regarding the same and I suggest you read them along with this article:
Honorary mention: useRef
hook
Many examples of memoization online - especially that of functions - use the useRef
hook. The useRef
hook works similarly to useMemo
. It makes sure that the value does not change between renders, although no dependency array can be supplied to useRef
array.
But, this is not the main use case of useRef
hook. useRef
is usually used to store references (hence the name) to an actual DOM element.
Conclusion
In conclusion, optimizing React applications is crucial for performance, and mastering memoization techniques can significantly enhance efficiency. By understanding the rendering process in React and using memoization methods like useMemo
, useCallback
, and memo
, you can prevent unnecessary re-renders and boost your app's performance. However, it's essential to use memoization wisely and explore other optimization techniques, such as better component design and local state management, before resorting to memoization.