Composition Based Optimization: Minimizing Rerenders in React

by Liben Hailu
4 mins read

Minimize unnecessary rerenders in your React components by strategically composing their structure. Discover simple yet often overlooked techniques to enhance performance in your React application.


Moving State Down

In React, when a state change occurs, it triggers a rerender of the component. This rerender propagates from the tree where the state change happened to the bottom. Let's observe this in action.

App.tsx
export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveComponent />
      <SomeOtherExpensiveComponent />
    </div>
  );
}

Now, this component and its child components rerender whenever the state changes, resulting in noticeably slow interactions with the page.

Now, how can we address this issue? Since our expensive components are not directly dependent on that specific state, they can be extracted into their own separate component, allowing them to rerender independently without affecting our expensive component.

Counter.tsx
export default function Counter(){
    const [count, setCount] = useState(0);
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
}

The app component now looks like this

App.tsx
export default function App(){
    const [count, setCount] = useState(0);
    return (
        <div>
            <Counter/>
            <ExpensiveComponent />
            <SomeOtherExpensiveComponent />
        </div>
    );
}

Now, only the counter component will rerender whenever the state is updated.

Children as a prop Pattern

Moving state down isn't always easy and possible. Occasionally, the wrapper component may require both the state and the expensive components. In such scenarios, utilizing Children as a prop can help mitigate rerender issues.

WrapperComponent.tsx
const WrapperComponent = ({children}) => {
    const [count, setCount] = useState(0);
    return (
        <div>
             <h1>{count}</h1>
            <button onClick={() => setCount(count + 1)}>Increment</button>
            {children}
        </div>
    );
}

and out App component looks like.

App.tsx
export default function App(){
    const [count, setCount] = useState(0);
    return (
        <WrapperComponent>
            <ExpensiveComponent />
            <SomeOtherExpensiveComponent />
        </WrapperComponent>
    );
}

Even though the state change is triggered in the parent component, the expensive components won't undergo rerendering. The react diffing algorithm can easily recognize that ComponentBeforeRender and ComponentAfterRender are the same, and prevents from going into a deeper rerendering process. However, you have to ask yourself if this is what you want to do when passing children as a prop, as it might lead to losing control over the components that will be passed. It is especially important to avoid it in components where there has to be some control. Still, if passing children as a prop is what you want, even though it is easy to make mistakes, you can clone the original component props and merge them with the property you want using React.CloneElement and then render the cloned component.