DEV Community

Isteak Ahammed Shupto
Isteak Ahammed Shupto

Posted on

A detail guide on optimizing the re-rendering issue in React's Context API

We have all used context API when building software using ReactJS. One of the common problem of context API while using in large applications that when a context value changes, all components that consume that context triggers re-rendering, regardless of whether the components actually uses the specific part of the context that changed. This leads to performance issues for mid to large sized applications with complex component trees.

Why this happens? React doesn't automatically know or track which specific part or properties (like states) a component uses. React only knows that a component uses some of parts or properties of the context.

So because of this reason and because of this type of system design in react's context api, react notifies all consumers or we can say all components that consumes those context properties when a context value changes. For this reason, every consumers or components get re-rendered.

Basically in more simple terms when a context value gets changed, below things happen:

  1. React schedules a re-trigger or re-render for all the components that
    • uses the useContext(Context) hook
    • Are wrapped in
    • Are descendants of a component that uses the context

First, we will demonstrate this problem via code. Use Vite to generate a react application, and put the below codes in the App.jsx file.

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

const AppContext = createContext();

function App() {
  const [state, setState] = useState({
    theme: { color: "blue", fontSize: 16 },
    user: { name: "John", id: 1 },
    settings: { notifications: true, language: "en" },
  });

  const changeTheme = () => {
    setState((prev) => ({
      ...prev,
      theme: {
        ...prev.theme,
        color: prev.theme.color === "red" ? "blue" : "red",
      },
    }));
  };

  return (
    <AppContext.Provider value={state}>
      <div>
        <button onClick={changeTheme}>Change theme</button>

        {/* All these components will re-render when any part of context changes */}
        <ThemeDisplay />
        <UserProfile />
        <SettingsPanel />
      </div>
    </AppContext.Provider>
  );
}

function ThemeDisplay() {
  const state = useContext(AppContext);
  console.log("ThemeDisplay re-rendered");

  return (
    <div style={{ color: state.theme.color }}>Theme: {state.theme.color}</div>
  );
}

function UserProfile() {
  const state = useContext(AppContext);
  console.log("UserProfile re-rendered");

  return <div>User: {state.user.name}</div>;
}

function SettingsPanel() {
  const state = useContext(AppContext);
  console.log("SettingsPanel re-rendered");

  return (
    <div>Notifications: {state.settings.notifications ? "On" : "Off"}</div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

First, inject the upper code into App.jsx and try to analyze and understand it properly. The code shows how React's context API causes all the consumers or the components using useContext to re-render whenever the context value changes, even if only part of the state is updated. The App component manages a global state object containing theme, user and settings and provides it to all children (ThemeDisplay, UserProfile and SettingsPanel) through the AppContext.Provider. A button exists in the App component which toggles the theme.color, which updates only the theme property in state. The childrens (ThemeDisplay, UserProfile and SettingsPanel) using the same AppContext through useContext. All of these children also contains a console.log which prints a confirmation output of them being re-rendered.

Now after adding the code to your react app in App.jsx, start the localhost server and open the react app in a window. Now open the Chrome developer tools in the same window where the react app is running. Now try clicking the change theme button.

After that, open the console tab and there you will see all the children components displaying console log output text that was written in them if the Change theme button is clicked. This indicates all of the children components being re-rendered even if only theme has changed.

To optimize this and reduce the re-rendering, one of the way is to turn the children components into standalone components and we need to pass only the relevant context data as props. We also need to wrap the children using memo from react. By this way, whenever the context updates the children won't re-render as long as the props used them haven't changed.

Now notice, analyze and try to understand the below code which follows this approach.

import { createContext, useState, memo } from "react";

const AppContext = createContext();

function App() {
  const [state, setState] = useState({
    theme: { color: "blue", fontSize: 16 },
    user: { name: "John", id: 1 },
    settings: { notifications: true, language: "en" },
  });

  const changeTheme = () => {
    setState((prev) => ({
      ...prev,
      theme: {
        ...prev.theme,
        color: prev.theme.color === "blue" ? "red" : "blue",
      },
    }));
  };

  const changeUser = () => {
    setState((prev) => ({
      ...prev,
      user: { ...prev.user, name: prev.user.name === "John" ? "Jane" : "John" },
    }));
  };

  const toggleNotifications = () => {
    setState((prev) => ({
      ...prev,
      settings: {
        ...prev.settings,
        notifications: !prev.settings.notifications,
      },
    }));
  };

  return (
    <AppContext.Provider value={state}>
      <div>
        <button onClick={changeTheme}>Change Theme</button>
        <button onClick={changeUser}>Change User</button>
        <button onClick={toggleNotifications}>Toggle Notifications</button>

        <ThemeContainer theme={state.theme} />
        <UserContainer user={state.user} />
        <SettingsContainer settings={state.settings} />
      </div>
    </AppContext.Provider>
  );
}

const ThemeContainer = memo(({ theme }) => {
  console.log("ThemeContainer re-rendered");
  return <ThemeDisplay color={theme.color} fontSize={theme.fontSize} />;
});

const UserContainer = memo(({ user }) => {
  console.log("UserContainer re-rendered");
  return <UserProfile name={user.name} id={user.id} />;
});

const SettingsContainer = memo(({ settings }) => {
  console.log("SettingsContainer re-rendered");
  return (
    <SettingsPanel
      notifications={settings.notifications}
      language={settings.language}
    />
  );
});

const ThemeDisplay = memo(({ color, fontSize }) => {
  console.log("ThemeDisplay re-rendered");
  return (
    <div style={{ color, fontSize: `${fontSize}px` }}>
      Theme: {color}, Font Size: {fontSize}px
    </div>
  );
});

const UserProfile = memo(({ name, id }) => {
  console.log("UserProfile re-rendered");
  return (
    <div>
      User: {name} (ID: {id})
    </div>
  );
});

const SettingsPanel = memo(({ notifications, language }) => {
  console.log("SettingsPanel re-rendered");
  return (
    <div>
      Notifications: {notifications ? "On" : "Off"}, Language: {language}
    </div>
  );
});

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, let's analyze how the upper code snippet implements the idea of passing standalone components and wrapping the children components using memo. First of all, replace the previous App.jsx code file to upper code snippet.

Now start the react localhost server and open it in a window and in the same window open the chrome developer tools. Now click any of the Change Theme, Change User, Toggle Notifications button and we will see that only relevant children component logged the output, which eventually indicates that only that specific children component got re-rendered and other children components didn't get re-rendered.

So, how our approach solved the re-rendering issue? First, we extracted the children as standalone components, we have ThemeContainer, UserContainer, SettingsContainer as children components. After that, instead of them using the entire AppContext using useContext, App passes only the relevant slice of states for example here the theme, user or settings.

If we had passed the entire state to every child components, then even when only one property for example the theme color changes, react would have treated the state object as new. This means all the children components would receive a new prop reference and they would get re-rendered, even if the user or settings part of the state hadn't changed.

By extracting the standalone components and by passing them only the exact piece of state they actually need, react now can do a much better and finer comparison. For example, here the ThemeContainer only gets the theme object, so changing the user or settings doesn't affect it all. And also notice that to avoid un-necessary re-renders, we have also used memo to wrap the children and because of the usage of this memo, a children component will not re-render if it's props haven't change, even though the parent App re-renders when the overall state changes.

So, the final outcome? Now changing the theme will not trigger UserProfile / SettingsPanel to re-render. And changing the user will not trigger ThemeDisplay or SettingsPanel to re-render. So overall, only the relevant components get re-rendered, exactly as we intended to optimize the re-rendering issue.

Top comments (0)