Optimizing React Context and Re-renders

Optimizing React Context and Re-renders

React Context and Re-renders

In this post, we'll dive into how re-renders happen in the situation of React Context.

I want us to understand how it works and also how you can optimize if needed.

Recap of using Context

Let's quickly look at some code to remind ourselves how Context is used:

// DashboardContext.js
import { createContext, useContext } from 'react'

type DashboardContextType = {
  name: string
  age: number
}

const DashboardContext =
  createContext<DashboardContextType | null>(null)

type DashboardProviderType = {
  children: React.ReactNode
  name: string
  age: number
}

export const DashboardProvider = ({
  children,
  name,
  age
}: DashboardProviderType) => {
  return (
    <DashboardContext.Provider value={{ name, age }}>
      {children}
    </DashboardContext.Provider>
  )
}

export const useDashboardContext = () => {
  const context = useContext(DashboardContext)

  if (!context) {
    throw new Error('useDashboardContext must be used within an DashboardProvider')
  }

  return context
}

How to use the provider:

// App.js

<DashboardProvider name="John" age={20}>
  <Dashboard />
</DashboardProvider>

The user values name and age are passed to the provider. They would in a real scenario e.g. be fetched from an API.

In Dashboard, we can use the useDashboardContext hook to access the values:

// Dashboard.js

const { name, age } = useDashboardContext();

You can also have other components use the useDashboardContext hook to access the values. They would have to be inside the DashboardProvider component. So next to Dashboard component itself or inside of it.

When will a component re-render?

Any component that consumes the context via useContext will re-render when the context changes.

If the component is inside the DashboardProvider but doesn't consume the context, it won't re-render when the context changes.

Oops, we have an issue

Many components are consuming the context and re-render whenever the context changes.

We want to minimize the re-renders as it's getting expensive.

Let's go over things you can do in this situation.

Evaluate what's causing the re-renders

Start by understanding the value of your context.

Pin down why re-renders are happening and when they're happening.

I recommend using the React DevTools to do this.

Split the context

If you've have a single context with a dozen of values, you can look at which components need what specific value.

Some components are related and are supposed to re-render together while others are completely unrelated.

Split the context into multiple contexts if they have values that are unrelated.

A sign here is to see which components are using the same values from the context.

Use the useMemo hook

You can use the useMemo hook to memoize the values of the context.

This can help when you've one Provider, but different states. Mind you, you'll have to create multiple consumption hooks if you take this approach.

For example:

import React, { createContext, useContext, useState, useMemo } from "react";

const DashboardContext = createContext();

export function DashboardProvider({ children }) {
  const [userInfo, setUserInfo] = useState({
    name: "Alice",
    email: "alice@example.com",
  });
  const [systemMetrics, setSystemMetrics] = useState({ cpu: 50, network: 20 });

  const userInfoValue = useMemo(
    () => ({
      ...userInfo,
      setUserInfo,
    }),
    [userInfo]
  );

  const systemMetricsValue = useMemo(
    () => ({
      ...systemMetrics,
      setSystemMetrics,
    }),
    [systemMetrics]
  );

  return (
    <DashboardContext.Provider value={{ userInfoValue, systemMetricsValue }}>
      {children}
    </DashboardContext.Provider>
  );
}

export function useUserInfo() {
  const context = useContext(DashboardContext);
  return context.userInfoValue;
}

export function useSystemMetrics() {
  const context = useContext(DashboardContext);
  return context.systemMetricsValue;
}

Every component using useUserInfo would only re-render when the userInfo changes. Same goes for useSystemMetrics.

This is also called "selective rendering". Specific hooks to only return what the component needs. If other data changes outside of what the specific hooks return, the component doesn't re-render.

State from above

If you've the issue where state comes from above the component that uses the Provider, you can use the React.memo function to memoize the component itself so that it doesn't re-render when its props changes.

This from my experience is rarely an issue, but worth mentioning:

const ConsumerComponent = React.memo(function ({ value }) {
  // Component implementation
});

useMemo on the value passed to Provider

You may be creating a new object every time the component re-renders, which causes the Provider to believe the value has changed, and then it re-renders all of its components.

Be careful, this also happens if you inline the Provider value.

// Bad
value={{ name, age }}

// Good
const value = useMemo(() => ({ name, age }), [name, age]);

Further read

If you want to learn more fancy stuff, you may want to read the docs about Scaling Up with Reducer and Context.