How to Reduce Unnecessary Re-renders

Nir Avraham
Welldone Software
Published in
3 min readNov 11, 2019

Building a large scale React app may include some re-renders headaches. The growth of your app may cause you to spend a lot of time investigating why “heavy” components re-rendered much more then you expected.

In this article, I’ll demonstrate a common use-case and will explain how placing useState / useSelector in the right component can optimize performance.

How does React work?

when the state of a component changes, it triggers the component’s re-render.

For example (codeSandbox):

const App = () => {
const [counter, setCounter] = useState(0);
return (
<div>
<button onClick={() => setCounter(counter + 1)}>Counter</button>
<Component1 />
<Component2 />
<Component3 />
</div>
);
};

After clicking the button, the state changes and App, Component1, Component2 and Component3 rerender.

For further reading checkout Vitali Zaidman’s article — React Element’s “Parent” vs “Rendered By”

So why does it matter to me?

Here is a real-life story:

In a project I’ve been working on, we had a Page component:

const Page = () => {
const [isSticky, setIsSticky] = useState(false);

<Header isSticky={isSticky} />
<BigComponent />
}

The Page component held the state isSticky which affected the behavior of the Header. This state changed every time the user scrolled the Page. The isSticky state was drilled to the Header component.

This app performed badly — every time the user scrolled, both the Header and the BigComponent were rerendered.

NOTE: We could make the BigComponent pure (using React.memo) so it would never re-render, but it would require the component to check its props every time.

The solution — change the location of useState

The solution here was really easy.

I’ve relocated the useState hook into the Header component, which resulted in the Header component to be the one that handles setIsSticky and it’s the only one to re-render.

Now, every time a user scrolls the page, only the Header will rerender and the BigComponent will not.

const Page = () => (
<>
<Header />
<BigComponent />
</>
)
const Header = () => {
const [isSticky, setIsSticky] = useState(false);
...
}

Simple, isn't it?

What about Redux?

The useSelector (or the connect HOC) hook subscribes to the store and listens to changes in the state tree. As a result, each change of the Redux state propagates updates through the subscriptions.

The mechanism of Redux assures us that for each useSelector we use — only the components that use “useSelector”, and only if the state the hook returns changes, the component would re-render. All the components it renders and their children would re-render as a result.

const App = () => {
const dispatch = useDispatch();
const counter = useSelector(state => state.counter);
return (
<div>
<button onClick={() => dispatch(increaseCounter()}>{counter}</button>
<Component1 />
<Component2 />
<Component3 />
</div>
);
};

So the advice regarding the location of useState from the previous paragraph holds for useSelector as well.

const App = () => (
<div>
<Button />
<Component1 />
<Component2 />
<Component3 />
</div>
)
const Button= () => {
const dispatch = useDispatch();
const counter = useSelector(state => state.counter);
...
}

Conclusion

The problem and the solutions I’ve provided may seem trivial but sometimes while working and using states and selectors we don’t think about optimization and performance at all. On the other hand, thinking about it too much is also considered a bad practice.

Thanks for reading 😁

--

--