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)