Photo by Joshua Reddekopp on Unsplash
Prevent Unnecessary Re-Renders: A Guide to Using memo, useMemo, and useCallback
Intro
In this second installment of our React optimization series, we delve into an additional technique to boost your application's performance. In our previous article, we explored how a well-architected system that employs props and children can prevent unnecessary re-renders.
However, there are some scenarios where this approach falls short. In such cases, React.memo
can prove to be a valuable tool.
React.memo is a powerful solution that can optimize your application's performance by preventing unnecessary re-renders of components. By default, React re-renders a component whenever its state or props change. Even if only a small part of the component changes, the entire component is re-rendered. This can be especially taxing on performance, especially when rendering large or frequently used components.
memo
allows you to cache the result of a function and reuse it unless the input (props) changes. Consequently, if the props of a component remain the same, React will utilize the cached result of the function and sidestep the need to re-render the component. This can considerably enhance the performance of your application.
This can be wrapping the component we want to memoize (in this case the Info) with React.memo:
const Form = () => {
const [value, setValue] = useState('');
return (
<>
<input type='text' onChange={(e) => setValue(e.target.value)} />
<MemoizedInfo name='fer-codes' />
</>
);
};
const MemoizedInfo = React.memo(({ name }) => {
console.log('I was rendered') <--- will only be called once!
return (
<>
<p>{name} accepts to pay the ammount in full</p>
</>
);
});
In this scenario, React.memo effectively prevents the component from re-rendering when the parent undergoes state updates since the name
prop of the component remains constant.
To elaborate, consider a memoized component as a self-regulating entity that determines when to re-render, based on its own criteria. This could range from never re-rendering to re-rendering only when specific props change.
Now, let's explore the outcome if we alter the props' structure by using an object instead of a string:
const Form = () => {
const [value, setValue] = useState('');
// update the shape of the props to be an object
const info = { name: 'fer-codes' };
return (
<>
<input type='text' onChange={(e) => setValue(e.target.value)} />
<MemoizedInfo info={info} />
</>
);
};
const MemoizedInfo = React.memo(({ info }) => {
console.log('I was rendered') <--- Will be called on every update from the parent
return (
<>
<p>{info.name} accepts to pay the ammount in full</p>
</>
);
});
It may come as a surprise that the Info
component undergoes re-rendering every time the input
is updated, i.e., every update from the parent component.
One might ask, "Why does this occur if the name
value is constant?"
Indeed, while the value remains the same, the reference to the Info
object is not constant. This indicates that React considers every update to be associated with a new object. As a result, the memoized Info
component's prop changes, and it re-renders.
This phenomenon arises since React utilizes referential equality to compare props.
Remember this to follow this vital rule when using memo
:
It is recommended to memoize every prop that is not a primitive data type.
A quick lesson about Primitives vs None-Primitives values in JavaScript
JavaScript has 7 primitive data types, which are data that are not objects and do not have any properties or methods associated with them. Those primitives are: string
, number
, boolean
, undefined
, null
, symbol
and bigint
.
When we compare primitive values, we compare them by their value:
const name1 = 'fer-codes'
const name2 = 'fer-codes'
name1 === name2 ✅
true // console output
However, the scenario changes significantly for objects
. Unlike primitive data types, objects are reference types, implying that they are stored and transmitted by reference, rather than by value.
When you create an object in JavaScript, a reference to that object is established, instead of generating a duplicate copy of the object.
Since objects are compared by their reference value, this returns false.
const info1 = { name: 'fer-codes' }
const info2 = { name: 'fer-codes' }
info1 === info2 ❌
false // console output (not the sabe object in memory)
This same rule applies to Objects
, Arrays
or Functions
since all of them are considered objects in JavaScript.
Memoizing none-primitives
React's useMemo
hook allows you to memoize a function and store its result in cache, ensuring that the function is only re-computed when its dependencies change. The useMemo
function receives two arguments: the computation function and an array of dependencies on which the computation relies.
For example:
const Form = () => {
const [value, setValue] = useState('');
// memoize the "info" object
const info = useMemo(() => { name: 'fer-codes' }, []);
return (
<>
<input type='text' onChange={(e) => setValue(e.target.value)} />
<MemoizedInfo info={info} />
</>
);
};
In this case the info
object will always be the same since we aren't passing any dependencies to the array.
Now let’s say that we want to pass a function
as a prop to the Info
component. In that case, it is necessary to memoize functions as well if you want to avoid recreating them every time the component renders.
In such situations, we can utilize the useCallback
hook.
const Form = () => {
const [value, setValue] = useState('');
const info = useMemo(() => { name: 'fer-codes' }, []);
const submit = useCallback(() => { /* your logic here */ }, []);
return (
<>
<input type='text' onChange={(e) => setValue(e.target.value)} />
<MemoizedInfo info={info} submit={submit} />
</>
);
};
The effect of using useCallback
is similar to that of useMemo
. In this case, the function will not be re-created unless an item in the dependency array useCallback
is modified. As we don't have any dependencies in this example, the submit
function will be generated just once.
In the end, the optimized code should look like this:
const Form = () => {
const [value, setValue] = useState('');
const info = useMemo(() => { name: 'fer-codes' }, []);
const submit = useCallback(() => { /* your logic here */ }, []);
return (
<>
<input type='text' onChange={(e) => setValue(e.target.value)} />
<MemoizedInfo info={info} submit={submit} />
</>
);
};
const MemoizedInfo = React.memo(({ info, submit }) => {
return (
<>
<p>{info.name} accepts to pay the ammount in full</p>
<button onClick={submit}>Submit</button>
</>
);
});
After using React.memo
in combination with useMemo
and useCallback
, we are ensuring that the MemoizedInfo
the component will not be re-rendered on state updates from the parent component (Form) that are in this case unrelated to the MemoizedInfo
component.
By using React.memo
together with useMemo
and useCallback
, we guarantee that the MemoizedInfo component won't be re-rendered due to state updates from the parent component (Form) that are unrelated to the MemoizedInfo component.
Custom props comparison: arePropsEqual
By default memo
will do a shallow
comparison between the old props and the new props, In other words, it checks if the new property is reference-equal to the previous one. So, even if the components within the object or array are identical, if a new object or array is created each time the parent is re-rendered, it will still be evaluated as a new property. That is why is important to memoize objects
and functions
as we saw before, you can read more about shallow copies here.
Suppose you need to compare the old and new props in a way not covered by the default shallow equality comparison behavior. In that case, you can pass a custom comparison function as the second argument to the memo
function. This function should only return true
if the new props would result in the same output as the old props. Otherwise, it should return false
.
const Info = React.memo(({ info, submit }) => {
return (...);
});
const arePropsEqual = (prevProps, nextProps) => {
return nextProps.info.name === prevProps.info.name;
};
const MemoizedInfo = memo(Info, arePropsEqual);
This can be very useful when you want the component to only re-render based on specific prop changes.
One final piece of advice when using a custom comparison function is to avoid conducting deep equality checks. Performing extensive equality checks on data structures can significantly slow down your application if it is modified later, potentially causing it to freeze for several seconds. If you're properly memoizing
objects and functions, it's unlikely that you'll need to conduct a deep equality check.
Conclusion
In conclusion, optimizing React applications is a crucial step in ensuring their performance and scalability. React.memo
, useMemo
, and useCallback
are useful tools that can be leveraged to achieve this optimization.
Memoization helps to prevent unnecessary re-renders and it's recommended to memoize every prop that is not a primitive data type.