Introduction:
In a React application, components are rendered…usually a lot. Improving performance includes preventing unnecessary renders and reducing the time a render takes. React comes with tools to help us prevent unnecessary renders: memo
, useMemo
, and useCallback
.
No change, no re-render:
When the parent's state (or props) changes, react will re-render all the children components even though their props or state are the same.
Have a look at the following code:
import React, { useRef, useState } from "react";
const Cat = ({ name }) => {
console.log(name, " rendered!");
return <li>{name}</li>;
};
const App = () => {
const [cats, setCats] = useState(["Boggy", "Siana"]);
const inputRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
setCats([...cats, inputRef.current.value]);
inputRef.current.value = "";
};
return (
<>
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
type="text"
required
/>
<button type="submit">Add cat</button>
</form>
<ol>
{cats.map((name, index) => <Cat key={index} name={name} />)}
</ol>
</>
);
}
Let's add a new cat Picka
and see what the console logs:
Boggy rendered!
Siana rendered!
Picka rendered!
Every time we add a new cat and update the state, react re-renders all the children components:
The Cat
component is a pure component: same output given the same props. We don’t want to re-render a pure component if the properties haven’t changed. React provides us with a solution: The memo()
function can be used to create a component that will only re-render when its properties change.
/* import the `memo` function */
import React, { memo, useRef, useState } from "react";
const Cat = ({ name }) => {
console.log(name, " rendered!");
return <li>{name}</li>;
};
/* wrap the `Cat` component within `memo` */
const MemoCat = memo(Cat);
We’ve created a new component called MemoCat
. MemoCat
will only cause the Cat
to render when the props change.
Then, you can replace the cat
component with MemoCat
.
...
{cats.map((name, index) => <MemoCat key={index} name={name} />)}
...
Now, every time we add a new cat, we see only one render in the console:
picka rendred!
useCallback:
Now, what if we need to pass a function to the Cat
component along with the name
prop? Ok, let's modify the Cat
component:
const Cat = ({ name, meow = f => f }) => {
console.log(name, " rendered!");
return <li onClick={() => meow(name)}>{name}</li>;
};
const MemoCat = memo(Cat);
Note: the f => f
is a fake function that does nothing. It is used to avoid errors in case we didn't provide a meow
property to the Cat
component.
Now, let's modify the App
and provide a meow
prop:
const App = () => {
const [cats, setCats] = useState(["Boggy", "Siana",]);
const inputRef = useRef();
// create a function that we will pass to the Cat component as a prop.
const meow = (name) => console.log(name, 'meowed!');
const handleSubmit = (event) => {
event.preventDefault();
setCats([...cats, inputRef.current.value]);
inputRef.current.value = "";
};
return (
<>
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
type="text"
required
/>
<button type="submit">Add cat</button>
</form>
<ol>
{cats.map((name, index) => <MemoCat
key={index} name={name} meow={meow} />
)}
</ol>
</>
);
}
export default App;
Let's retry adding a cat named 'picka' to the list and see what is logged in the console:
Boggy rendered!
Siana rendered!
picka rendered!
Unfortunately, the magic of memo
is gone. But why???
Every time we update the state, the App
re-renders and the meow
function is re-created. For React, the new meow
function prop is different from the previous one. So it re-renders the Cat
component.
To solve this problem, we have to 'memoize' (or simply remember) the meow
function definition between re-renders. That's why we use useCallback
hook.
Let's import useCallback
:
import {useCallback} from 'react'
and use it in our App
component to:
const meow = useCallback((name) => console.log(name), [])
It works as expected when adding a new Cat!
Heavy calculation? cache it:
Rendering takes time and resources, especially if your component is doing heavy calculations, like looping over a large array or doing expensive computations.
React provides us with the useMemo
hook, which let's us cache the result of a calculation between re-renders.
useMemo
takes two parameters, the 1st one is a function, and a dependency array of values needed in the calculation, it will re-run the function only if the dependency array changes. if the dependency array didn't change and the component re-renders, useMemo won't re-calculate the value and it will return the one from the previous render instead.
Bonus: How to determine if your component is doing heavy calculations:
To know if your component is doing heavy tasks, you may need to measure the time spent on a piece of code using console.time()
.
console.time('filter array')
const filteredArray = useMemo(() => {...}, [deps])
console.timeEnd('filter array')
if your function needed a significant amount of time to execute (say 1ms
or more), you may need to use useMemo
That's all for today! I hope you enjoyed reading this article.