Skipping renders and memoization in React
In many situations, a React component will re-render when it doesn’t have to. If the result of rendering would be exactly the same as the previous time the component rendered, skipping that render (reconciliation) step entirely is desirable.
Class based components
shouldComponentUpdate
In class components, the method shouldComponentUpdate
allows this.
It’s a lifecycle method that is called before render()
. The method returns a boolean. That boolean tells React if render()
can be skipped.
When true
, the render()
will be executed like it normally would.
When false
, that tells React it can skip executing the render()
.
shouldComponentUpdate()
is called with the next props and the next state. This allows complex logic where the current props/state are compared to the previous props/state in order to determine if the output would be different and thus, the component should update.
By default, shouldComponentUpdate()
returns true
.
Not specifying this method at all is the same as
shouldComponentUpdate(nextProps, nextState) {return true}
Don’t rely on this to prevent rendering altogether. It might work right now, but it can lead to bugs and is likely to change in the future. Instead, treat it as a hint to React that tells it “you can safely skip rendering this, the result will be the same as the previous result anyway”.
The logic in shouldComponentUpdate
can quickly get very complex and is prone to mistakes.
Before you know it, that method will look something like this
shouldComponentUpdate(nextProps, nextState) {const propsComparison = this.props.a !== nextProps.a && this.props.b !== nextProps.b && this.props.c !== nextProps.c && this.props.d !== nextProps.dconst stateComparison = this.state.one !== nextState.one && this.state.two !== nextState.two && this.state.three !== nextState.threereturn propsComparison && stateComparison}
😢 I just wanted to check if any props or state changed, why is that so hard?
React.PureComponent
React.PureComponent
does exactly that! 😎
PureComponent
performs a shallow comparison of props and state (by using Object.is).
This reduces the chance that you’ll skip a necessary update (e.g. when you add a new prop).
Unless you are confident you need a custom shouldComponentUpdate
, prefer PureComponent
.
That means these two snippets are equivalent
class Driver extends React.Component {shouldComponentUpdate() {// a shallow comparison of all the props and state}render() {<p>{this.props.name}</p>;}}
class Driver extends React.PureComponent {render() {<p>{this.props.name}</p>;}}
Function components
When trying to apply that same optimization to function components instead of class based ones, a problem rears its head. Function components can’t really skip that render step. The function component (which is really just a function) is either executed or it isn’t.
This is where memoization helps.
Memoization is basically technobabble for remembering something for later.
React can’t just remember pieces of data for later, it can remember entire components.
React.memo
React.memo
does this!
What the previous two examples were for class based components, React.memo
is for function components.
Instead of skipping the render-step like in class based components, React.memo
will reuse the last rendered result instead of calculating a new result.
// the function componentconst Driver = function (props) {return <p>{props.name}</p>;};// exporting the memoized function componentexport default React.memo(Driver);
- Initial render of the memoized Driver component with props
{ name: "Charles Leclerc" }
- The function component renders
<p>Charles Leclerc</p>
.
- The function component renders
- The props change to
{ name: "Daniel Ricciardo" }
- The components renders
<p>Daniel Ricciardo</p>
- The components renders
- Something else changes that triggers an update to our Driver component
React.memo
sees that the props haven’t changed.- Instead of calculating the render result, React uses the previous result:
<p>Daniel Ricciardo</p>
By default React.memo
is comparable to React.PureComponent
as it performs a shallow comparison of all props (by using Object.is again).
If you want more control and be in charge of that comparison, React.memo
accepts a second argument, a comparison function. This makes it comparable to shouldComponentUpdate
in class based components.
The comparison function also returns a boolean.
That boolean tells React if it should use the previous result of the component instead of calculating a new one.
When false
, the function component will be executed like it normally would.
When true
, the function component will not be executed and the previous result will be used instead.
The comparison function is called with the previous props and the next props. This allows complex logic where the current props are compared to the previous props in order to determine if the output would be different and thus, the remembered result/memo of the component should be used.
// the function componentconst Driver = function (props) {return <p>{props.name}</p>;};// the custom comparison functionconst comparisonFn = function (prevProps, nextProps) {return prevProps.name === nextProps.name;};// exporting the memoized function componentexport default React.memo(Driver, comparisonFn);
To extend the parallels with class based components: Unless you are confident you need a custom comparison function, prefer the default behaviour.
Examples
In this sandbox demo there is a toplevel component with 2 pieces of state, a count
and an unusedCount
. Which, as the name suggests, will remain unused 🙃.
You can increment the count
and the unusedCount
through buttons.
The top component has 4 children, all of them will display the count
and how many times that child component rendered.
The components that have one of the optimizations bescribed above will only render when count
is updated. The other ones will also render when the unusedCount
is updated.
React.memo
vs React.useMemo
While React.memo
is a higher-order component as it accepts a component and returns the new/memoized component.
React.useMemo
is a hook(which is a function). It accepts a function and returns the memoized return value of the function you passed.
React.useMemo
const memoizedValue = React.useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.useMemo
accepts a function as first argument. The value this function returns is the value that React.useMemo
will return. It will only be calculated again if it has to. React.useMemo
will return the memoized/remembered value if it doesn’t.
You tell React.useMemo
if it should recalculate that result through the second argument, an array. The value the passed function returns will only be calculated again if something in that array of dependencies changes. Not passing anything would cause that value to be calculated every time the component renders (and causes the function to run).
Every value used inside the function you passed should be included in the dependencies array. This will prevent a lot of unintended behaviour.
The React team has created an ESLint package, eslint-plugin-react-hooks
that is designed to warn you when breaking the rules of hooks. The dependencies array being complete is checked by a rule in that package called exhaustive-deps
.
Example
import React from "react";function calculatePodiums(name) {// very expensive calculationreturn numResult;}const Driver = function (props) {const numOfPodiums = React.useMemo(() => calculatePodiums(props.name), [props.name,]);return (<div><p>My name is: {props.name}</p><p>I drive for: {props.team}</p><p>I have been on the podium {numOfPodiums} times</p></div>);};
- Initial render of our Driver component with props
{ name: "Kimi Räikkönen", team: "Ferrari" }
- The function component calculates
numOfPodiums
and renders using the result of that calculation.
- The function component calculates
- The props change to
{ name: "Kimi Räikkönen", team: "Alfa Romeo Racing" }
React.useMemo
sees nothing in the dependencies array has changed and does not recalculatenumOfPodiums
- The memo/remembered value for
numOfPodiums
is used.
- The props change again to
{ name: "Antonio Giovinazzi", team: "Alfa Romeo Racing" }
React.useMemo
sees something changed in the dependencies array and calculatesnumOfPodiums
- The freshly calculated value is used.
Bonus: React.useCallback
This is a shortcut for a specific React.useMemo
usage.
React.useMemo
returns a memoized value
React.useCallback
returns a memoized function
🤔 But a value can totally be a function!
Correct! That means these two snippets are equivalent
const memoizedFunction = React.useMemo(function() {return function doTheThing(a, b) {// do the thing}}}, [a, b])
⬆ This memoizes the value the first argument (a function) returns, which is a function called doTheThing
.
const memoizedFunction = React.useCallback(function doTheThing(a, b) {// do the thing}}, [a, b])
⬆ This memoizes the first argument, which is a function called doTheThing
.
Like React.useMemo
, the second argument is an array of dependencies.
The function React.useCallback
returns will only change when something in that array changes.