Before we dive deep into these optimizations we must understand why components re-render in React.
There are 3 reasons a component will re-render:
State changes — these occur when your component’s state is altered.
Parent re-render — a parent component’s state changes, leading to the recursive re-rendering of all its children.
Context value changes — this occurs when the values in the context, which components share, are modified.
If you want a deep understanding of how rendering works you should check out the official documentation.
Now that we’ve got a handle on why re-rendering happens, let’s move on to discussing on how we can improve performance.
Passing state down
Let’s look at the following example. Can you spot the issue?
export function Component() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open</button>
{isOpen ? <Popover> ? null}
<HeavyComponent />
<ReallyHeavyComponent />
</div>
)
}
We intend to trigger the popover when the button is clicked. It might look fine but whenever the isOpen
state changes the component re-renders its children that don't change, which leads to poor performance. So how can we fix this?
We could use memo
to memoize the components and prevent unnecessary re-renders like this:
const MemoizedHeavyComponent = memo(HeavyComponent);
const MemoizedReallyHeavyComponent = memo(ReallyHeavyComponent);
export function Component() {
return (
<div>
<ButtonWithPopover />
<MemoizedHeavyComponent />
<MemoizedReallyHeavyComponent />
</div>
);
}
While this works, there is a better solution. We can simply move the state into its own component.
export function ButtonWithPopover() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open</button>
{isOpen ? <Popover> ? null}
</>
);
}
Now whenever the state changes, only the ButtonWithPopover
component will be re-rendered. So the final code will look like this
export function Component() {
return (
<div>
<ButtonWithPopover />
<HeavyComponent />
<ReallyHeavyComponent />
</div>
);
}
Components as children prop
Consider the following example:
export function Component() {
const [scrollY, setScrollY] = useState(0);
return (
<div
onScroll={({ currentTarget }) =>
setScrollY(currentTarget.scrollTop)
}
>
<HeavyComponent />
<ReallyHeavyComponent />
</div>
);
}
Again, changing the state re-renders the heavy components which is unnecessary. We can apply the previous optimization with a slight modification.
export function WithScroll({ children }) {
const [scrollY, setScrollY] = useState(0);
return (
<div
onScroll={({ currentTarget }) =>
setScrollY(currentTarget.scrollTop)
}
>
{children}
</div>
);
}
Here we pass our heavy components as children
and they will be unaffected when state changes inside WithScroll
.
export function Component() {
return (
<WithScroll>
<HeavyComponent />
<ReallyHeavyComponent />
</WithScroll>
);
}
Why does this work? Because children
is just syntax sugar for passing the prop children
, so this means that the parent, in this case Component
has to re-render for it to do aswell. We can write the component from above like this:
export function Component() {
return (
<WithScroll
children={
<>
<HeavyComponent />
<ReallyHeavyComponent />
</>
}
></WithScroll>
);
}
Components as props
What is wrong here?
export function Component() {
return (
<WithScroll
children={
<>
<HeavyComponent />
<ReallyHeavyComponent />
</>
}
></WithScroll>
);
}
When isCollapsed
changes the SlowComponent
and VerySlowComponent
have to re-render. To prevent this we can pass our components as props and it will work the same way children
do from Components as children prop.
export function Component({ menu, content }) {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<div className="wrapper">
<div className="menu">{menu}</div>
<div className="content">
<button onClick={() => setIsCollapsed(!isCollapsed)} />
{content}
</div>
</div>
);
}
And the final code is:
export function ParentComponent({ menu, content }) {
return (
<Component menu={<SlowComponent />} content={<VerySlowComponent />} />
);
}
Now, unless ParentComponent
re-renders, whenever state changes inside Component
, our menu
and content
props will be unaffected.
Enjoyed reading this post? Why not share it with your friends.
You can also find these posts on my blog in a better format.
Top comments (0)