A simple solution to optimize React re-renders
Without using: React.memo, useMemo, PureComponent, or shouldComponentUpdate
In this post, I want to share two alternative solutions to the list of approaches above. Before you reach for one of the methods above you should consider the two solutions below.
Slow Components
Imagine we have a component that takes a long time to render. For this example, we’ll refer to <SlowComponentTree />
as our slow component.
import { useState } from 'react';
export default function App() {
let [color, setColor] = useState('red');
return (
<div>
<input
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<p style={{ color }}>Hello, world!</p>
<SlowComponentTree />
</div>
);
}
function SlowComponentTree() {
let now = performance.now();
while (performance.now() - now < 100) {
// Artificial delay -- do nothing for 100ms
}
return <p>I am a very slow component tree.</p>;
}
The problem above is that whenever color
changes inside App
, we will re-render <SlowComponentTree />
which we’ve intentionally delayed to be very slow.
We could use one of the 4 methods listed at the beginning of this post, but that’s not the point of this article. I want to showcase two alternative solutions.
Solution 1: Move State Down
In we take a closer look at the children in App
we’ll notice that the color
state is only being used by a child of App
. Specifically, the <input>
and <p>
tag:
<input
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<p style={{ color }}>Hello, world!</p>
Lets move state down and extract this part into a separate component:
import { useState } from 'react';
export default function App() {
return (
<div>
<Form />
<SlowComponentTree />
</div>
);
}function Form() {
let [color, setColor] = useState('red');
return (
<>
<input
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<p style={{ color }}>Hello, world!</p>
</>
);
}
Now if the color
state changes, only the Form
component re-renders. Problem solved.
Solution 2: Lift Content Up
The solution above doesn’t work if the piece of state is used somewhere above the expensive tree. For example, let’s say we put the color
on the parent <div>
export default function App() {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<p>Hello, world!</p>
<SlowComponentTree />
</div>
);
}
Now it seems like we cannot “extract” the parts that don’t use color
into another component, since that would include the <div>
, which would then include <SlowComponentTree />
.
See if you can figure out how we could change the code above to prevent re-rendering <SlowComponentTree />
without using one of the 4 methods mentioned at the beginning of this post.
…
…
The answer is surprisingly simple:
export default function App() {
return (
<ColorPicker>
<SlowComponentTree />
</ColorPicker>
);
}function ColorPicker({ children }) {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<p>Hello, world!</p>
{children}
</div>
);
We create a separate component <ColorPicker>
and include the parts that depend on the color
state variable. The parts that don’t care about the color
state variable are passed to <ColorPicker>
using the children
prop.
When the color
state changes, <ColorPicker>
re-renders, but it still has the same children
prop it got from App
the previous time it rendered, so React doesn’t visit that subtree and doesn’t re-render <SlowComponentTree />
.
The important thing to take away from this solution is that if you give React the same element you gave it on the last render, it will not bother re-rendering that element.
Lessons Learned
Before you reach for methods like: React.memo, useMemo, PureComponent, or shouldComponentUpdate; look and see if you can split that parts that change from the parts that do not change.
Don’t neglect moving state down and lifting content up.