The Ultimate Guide to React Re-renders


Is your react app slow and sluggish? A very common reason react apps are slow is because of unnecessary rerenders.

What are react re-renders?

Re-renders are react’s way of changing the DOM to make sure it is in sync with our app’s state.

When a re-render is triggered, react does a series of computations:

  • React calls the component’s function(in the case of function components) and gets a new virtual DOM
  • React then compares the new virtual DOM with the previous virtual DOM to find the difference between the two
  • And finally, React applies the difference to the real DOM

Creating the new virtual DOM and comparing it to the old virtual DOM is can be slow and ultimately unnecessary if no difference is found.
In this blog post I will explain how to optimize our apps by minimizing re-renders.

First, a disclaimer

It is important to only optimize performance once we have a performance problem. Premature optimization means we waste developer’s time fixing problems our users don’t have when we could have been solving actual problems. It could also make our code less readable and potentially create new bugs without getting anything in return.

When are components rendered?

React will render your entire component when it is mounted. The second time a component is rendered is called a re-render. There are exactly 2 reasons why a component would re-render.

  1. The component’s state has changed. This includes context that the component is using.
  2. The component’s parent has re-rendered. This is great because it means the UI will always be synced to the app’s state and thus avoid bugs.

How to Prevent Unnecessary re-renders

move state down the component tree

When state changes, it’s component and all of it’s descendants re-renders. So moving the state down the component tree means less components will be re-rendered. React component tree Note this comes at a cost. Moving state down the tree means our components are less reusable. A component that only has to display the props it is given is reusable, but once that component also has state and logic (that previously existed in it’s parent component) we are breaking the separation of concerns rule because we are mixing business logic with presentational logic.

useRef for non rendered data

Sometimes we want to store data between re-renders, this is what is what useState is for. But there are rare cases when we don’t want the component to re-render when that data is changed. This is why we have useRef. useRef lets us store data that is not used for rendering. For example, storing DOM nodes or setInterval timer id.

const timerRef = useRef(null);

useEffect(() => {
  timerRef.current = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(timerRef.current);
}, []);

useRef is an escape hatch from react’s rules and should only be used for data that is not rendered.

React.memo

Let’s say we have the following code:

const Component = (data) => {
  return (
    <div>
      { data }
      <SlowComponent />
    </div>
  );
};

export default Component;

If our component has re-rendered, SlowComponent will also re-render but it will have the exact same output as before since all of it’s inputs have stayed the same.

So what can we do? first we must make sure SlowComponent is pure, meaning that for the same props it will always give the same output.
For example, this is a pure component:

const PureComponent = ({ message }) => {
  return <div>{message}</div>;
};

Here is an example of an impure component

const ImpureComponent = ({ message }) => {
  // new Date() will give a different result each time it is rendered
  return <div>{`${new Date()}:${message}`}</div>;
};

once we have a pure component we can wrap it in React.memo:

import React from 'react';

const PureComponent = ({ message }) => {
  return <div>{message}</div>;
};

export default React.memo(PureComponent);

Whenever this component’s parent is re-rendered, the component will only re-render if it’s props have changed.

React.memo pitfalls

overusing React.memo

You should only use React.memo once you have performance issues, profiled your app(with tools like react devtools) and found that your component is having unnecessary re-renders. Checking if the props changed takes time and makes re-renders ever so slightly slower. If you are adding that check and also not saving any re-renders, you just made your app slower. It is also less clean in my opinion to have that React.memo wrap your component.

passing objects as props

React compares the current props and the new props shallowly using Object.is. That’s okay for numbers and string, but bad for objects and other non-primitive values.

Object.is('hi', 'hi'); // true
Object.is(1, 1); // true
Object.is([], []); // false
Object.is({ hi: 1 }, { hi: 1 }); // false

If you are creating a non-primitive object every render and passing it as a prop, then the props comparison will always return false and you will re-render the memoized component every time.

const Component = ({count, setCount})=>{
  return <MemoizedComponent
    object={{value:count}}
    regex={/\d+/}
    onClick={() => setCount(prev => prev + 1)}
    style={{fontSize:'20px'}}
  >
}

Each one of those props is creating a new object and causing an unnecessary re-render. That includes inline styles!
The solution? useMemo for values and useCallback for functions. And if you can, move objects outside the function altogether

const regex = /\d+/;
const style = {fontSize:'20px'};
const Component = ({count, setCount})=>{
  const object = useMemo(() => ({ value: count }), [count]);
  const onClick = useCallback(() => setCount(prev => prev + 1), [setCount]);
  return <MemoizedComponent
    object={object}
    regex={regex}
    onClick={onClick}
    style={style}
  >
}

please don’t use inline styles

This also includes children!

<MemoizedParent>
  <SomeChild/>
</MemoizedParent>

This code seems fine but we might as well have done this:

<MemoizedParent children={React.createElement(SomeChild)}/>

children are just props, and we just learned to not pass as a prop non-primitive values that change on every render. In this case, React.createElement will be called every render and will give us a new element with a different reference every time. So to fix this, memoize the children:

const child = useMemo(()=> <SomeChild/>, [])
return (<MemoizedParent>
  {child}
</MemoizedParent>)

This is a lot less readable, so make sure it actually improves performance.

name later

Sometimes, a prop with the same value has a new reference each render and we can’t do anything about it. In this case, we can compare the old and new props ourselves instead of relying on react’s default compare function.
Pass React.memo a function that will take 2 arguments and return a boolean.

const Chart = React.memo(({ dataPoints })=> {
  // ...
}, arePropsEqual);

const arePropsEqual = (oldProps, newProps) => {
  return (
    oldProps.dataPoints.length === newProps.dataPoints.length &&
    oldProps.dataPoints.every((oldPoint, index) => {
      const newPoint = newProps.dataPoints[index];
      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
    })
  );
}

Make sure to only use this if you must. Deep comparison can be slow, and with large datasets it might even be faster to just let react re-render your component.

context pitfalls

objects in context

When a context provider is re-rendered and it’s value has changed, all components consuming it will re-render. How does react detect when the value has changed? using a shallow comparison, exactly like a memoized component’s props. So all the React.memo gotchas apply here as well, memoize non primitive values, don’t create a new object every render.

avoid large contexts

It is very common to see a context provider like this being passed multiple values:

<UserSettingsContext.Provider
  value={{
    fontSize,
    setFontSize,
    language,
    setLanguage,
    isDarkMode,
    setIsDarkMode,
  }}
>
  {children}
</UserSettingsContext.Provider>

And when we consume the context like so:

function LanguageDisplay() {
  const { language } = useUserSettings();

  return (
    <div>
      <h2>Current Language: {language}</h2>
    </div>
  );
}

export default LanguageDisplay;

This component will be re-rendered whenever any part of that context is updated, even if the part we are actually using hasn’t changed. while it is very convenient to store related data together, it results in unnecessary re-renders.

No amount of memoization will help here.

So how can we avoid the re-render? we can split the large context into multiple small contexts:

<FontSizeContext.Provider value={fontSize}>
  <SetFontSizeContext.Provider value={setFontSize}>
    <LanguageContext.Provider value={language}>
      <setLanguageContext.Provider value={setLanguage}>
      /* more providers... */
      <setLanguageContext.Provider>
    <LanguageContext.Provider>
  <SetFontSizeContext.Provider>
<FontSizeContext.Provider>

Great! we solved the render problem and our app is fast now. But this seems wrong. For every single value we have 2 contexts(1 getter and 1 setter). This can’t be right, There has to be a cleaner way.

use-context-selector

use-context-selector is an npm library for subscribing to specific values in a context.

const fontSize = useContextSelector(
  UserSettingsContext,
  (state) => state.fontSize
);

This component will only re-render when font size has changed. Personally, I would only use use-context-selector when my app is too small for a state management library like zustand but too big to only be using context.

Performance Power Tools

It is very easy to spot bottlenecks in small snippets of code. But in huge apps with hundreds of components you have no idea where to start. Here are some tools that helped me improve performance.

1. React devtools profiler

You have probably already installed the react devtools browser extension. It comes with a very useful profiler that lets you record an action and then see how long it took for each component to render. Another useful feature is being able to highlight all rerendered components. So you can interact with your app and see for yourself if any unrelated components are highlighted. You can read how to use it here.

2. Why Did You Render

A nice tool called Why Did You Render that will tell you when and why a memoized component was re-rendered unnecessarily. Read more about it here.

3. React Scan

A new and experimental tool that lets you click an element in your browser and while you are using your app will show you when it is being re-rendered, why, and other useful data like FPS. Unlike other tools, you can lock your view on a specific component and block out “noise” from the rest of your app. Read more about it here.

4. The React Compiler

An official tool by the react team that automatically adds useMemo, useCallback and React.memo to your code when needed. It is still in beta so be careful. Read more about it here

Summary

In conclusion, React re-renders are a crucial part of keeping your app’s UI in sync with its state, but unnecessary re-renders can quickly drag down performance and degrade the user experience. By understanding when and why re-renders happen, and applying strategies like moving state down the component tree, using useRef for non-rendered data, optimizing context, and leveraging tools like React.memo, you can drastically reduce unnecessary re-renders and boost your app’s performance.

However, it’s important to remember that performance optimization should be driven by actual profiling data. Premature optimization can lead to unnecessary complexity, so always measure and test before applying these techniques. React’s powerful tools, along with a thoughtful approach to component design, can help you build faster, more efficient applications.

By following the tips shared in this guide, you’ll be well on your way to mastering React performance and delivering a smoother experience to your users.