Memoisation is a technique used to optimise the performance of a function by caching its results. This is particularly useful when the function is computationally expensive and is called multiple times with the same arguments. Rendering React components is a good example of a computationally expensive operation that can benefit from memoisation.
In this article, we will explore how memoisation can be used in React to optimise the performance of functional components.
React provides a React.memo
function that can be used to memoise functional components. When a component is memoised, React will only re-render the component if its props have changed.
import { useState, memo } from 'react';
function ParentComponent() {
console.log('Rendering ParentComponent');
const [count, setCount] = useState(0);
const items = ['Item 1', 'Item 2', 'Item 3'];
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<MemoisedComponent items={items} />
</div>
);
}
function ItemList(props: { items: string[] }) {
console.log('Rendering ItemList');
return (
<ul>
{props.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
const MemoisedComponent = memo(ItemList);
export default ParentComponent;
When a component is memoised using React.memo
, React will compare the previous props with the new props to determine if the component needs to be re-rendered. React performs a shallow comparison of the props by comparing the prop values using the Object.is
function. This means, two values are the same if both point to the same reference/location in memory.
React.memo
relies on javascript's referential equality. Objects, arrays and functions are not compared by their contents but their references. So, every time theParentComponent
is re-rendered, theitems
array is re-created, leading to a re-render of theMemoisedComponent
even though the contents of the array have not changed.- Even if we do
<MemoisedComponent items={["Item1", "Item2", "Item3"]} />
, theitems
array is re-created every time theParentComponent
is re-rendered, leading to a re-render of theMemoisedComponent
. - If we have children inside a memoised component, the children will be re-rendered every time the parent is re-rendered, even if the parent is memoised.
- Same is the case with inline functions like
onClick={() => console.log("Hello")}
- Same is the case with passing inline styles like
style={{ color: "red" }}
We can get around all of these issues using stable props.
Stable Props are props that do not change between re-renders. Examples include:
- Functions/objects defined outside the component.
- using
useMemo
to memoise variables/objects. - using
useCallback
to memoise functions.
React provides a useMemo
hook that can be used to memoise variables or objects. This is useful when the value of a variable or object is expensive to compute and does not change frequently.
import { useState, useMemo, memo } from 'react';
function ParentComponent() {
console.log('Rendering ParentComponent');
const [count, setCount] = useState(0);
const memoisedItems = useMemo(() => ['Item 1', 'Item 2', 'Item 3'], []);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<ItemList items={memoisedItems} />
</div>
);
}
function ItemList(props: { items: string[] }) {
console.log('Rendering ItemList');
return (
<ul>
{props.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
const MemoisedComponent = memo(ItemList);
export default ParentComponent;
To prevent unnecessary re-renders, we can use the useMemo
hook to keep the reference stable. The useMemo
hook takes a function that returns the value to memoise and an array of dependencies. If any of the dependencies change, the function is re-executed to compute the new value.
In the example above, the memoisedItems
variable is memoised using the useMemo
hook. The useMemo
hook takes a function that returns an array of items and an empty array as dependencies. Since the dependencies array is empty, the function is only executed once when the component is mounted.
React provides a useCallback
hook that can be used to memoise functions. This is useful when a function is expensive to compute and does not change frequently.
import { useState, useCallback, memo } from 'react';
function ParentComponent() {
console.log('Rendering ParentComponent');
const [count, setCount] = useState(0);
const memoisedItems = useMemo(() => ['Item 1', 'Item 2', 'Item 3'], []);
const onItemClick = useCallback((item) => {
console.log(`Clicked on ${item}`);
}, []);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
<ItemList items={memoisedItems} fn={onItemClick} />
</div>
);
}
function ItemList(props: { items: string[], fn: (item: string) => void }) {
console.log('Rendering ItemList');
return (
<ul>
{props.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
const MemoisedComponent = memo(ItemList);
export default ParentComponent;
To make the functions stable, we can use the useCallback
hook. The useCallback
hook takes a function and an array of dependencies. If any of the dependencies change, the function (and its reference) is created again.
In the example above, the onItemClick
function is memoised using the useCallback
hook. The useCallback
hook takes a function that logs the clicked item and an empty array as dependencies. Since the dependencies array is empty, the function is only created once when the component is mounted.
useMemo
and useCallback
are very similar in their usage. As a thumb rule
- Use the
useMemo
hook, if you want to memoise variables/objects. (Remembers the value) - Use the
useCallback
hook, if you want to memoise functions. (Remembers the function itself)
Memoisation is a powerful technique that can be used to optimise the performance of React components. By memoising variables, objects, and functions, we can prevent unnecessary re-renders and improve the overall performance of our application. But remember, memoisation is not a silver bullet and should be used judiciously to avoid unnecessary complexity.
React 19 has eliminated the need for memoisation in most cases by introducing Concurrent Mode and automatic batching. However, memoisation can still be useful in certain scenarios, especially when working with legacy code or third-party libraries.