preface

CreateContext is an API provided by React for global state management. You can inject state through the Provider and obtain state through the Consumer or useContext API (useContext is recommended).

CreateContext makes communication between components easier, but can cause significant performance problems if used improperly. Below we discuss the causes of performance problems and how to optimize them.

Root cause of the performance problem

Let’s start with an example: createContext performance problem cause, and note the two problem points in this example.

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

const ThemeCtx = createContext({});

export default function App() {
  const [theme, setTheme] = useState("dark");
  /** * Performance problem cause: * The parent component rendering themectx.provider causes all child components to render */

  return (
    <div className="App">
      <ThemeCtx.Provider value={{ theme.setTheme}} >
        <ChangeButton />
        <Theme />
        <Other />
      </ThemeCtx.Provider>
    </div>
  );
}

function Theme() {
  const ctx = useContext(ThemeCtx);
  const { theme } = ctx;
  useWhyDidYouUpdate("Theme", ctx);
  return <div>theme: {theme}</div>;
}

function ChangeButton() {
  const ctx = useContext(ThemeCtx);
  const { setTheme } = ctx;
  useWhyDidYouUpdate("Change", ctx);
  // Problem 2: Unchanged value in value state causes component rendering
  console.log("The setTheme hasn't changed and I shouldn't have rendered it either!!");
  return (
    <div>
      <button
        onClick={()= >setTheme((v) => (v === "light" ? "Dark" : "light"))} > Change theme</button>
    </div>
  );
}

function Other() {
  // Problem 1: render child components independent of value state
  console.log("Other render. I shouldn't have rerendered it!!");
  return <div>Other component, to be fair, I shouldn't have rendered it!</div>;
}
Copy the code

Problem 1 (overall repeat rendering) :ProviderComponent wrapped child components are all rendered

Provider will cause all child components to be re-rendered each time the themectx. Provider component is rendered. This is because the React. CreateElement (type, props: {},…). Each time the component is created, props: {} is a new object.

Problem 2 (partial repeat rendering) : useuseContextCauses components to render

CreateContext is implemented according to the publish-subscribe model, and every time the Provider’s value changes, all components that use it (those that use useContext) are notified to re-render.

The solution

Above we analyzed the root cause of the problem, now we begin to solve the problem. Again, take a look at the optimized example: createContext performance optimization.

import { useState, useContext, createContext, useMemo } from "react";
import { useWhyDidYouUpdate } from "ahooks";
import "./styles.css";

const ThemeCtx = createContext({});

export default function App() {
  return (
    <div className="App">
      <ThemeProvide>
        <ChangeButton />
        <Theme />
        <Other />
      </ThemeProvide>
    </div>
  );
}

function ThemeProvide({ children }) {
  const [theme, setTheme] = useState("dark");

  return (
    <ThemeCtx.Provider value={{ theme.setTheme}} >
      {children}
    </ThemeCtx.Provider>
  );
}

function Theme() {
  const ctx = useContext(ThemeCtx);
  const { theme } = ctx;
  useWhyDidYouUpdate("Theme", ctx);
  return <div>{theme}</div>;
  // return <ThemeCtx.Consumer>{({ theme }) => <div>{theme}</div>}</ThemeCtx.Consumer>;
}

function ChangeButton() {
  const ctx = useContext(ThemeCtx);
  const { setTheme } = ctx;
  useWhyDidYouUpdate("Change", ctx);

  /** * Solution: use useMemo ** /
  const dom = useMemo(() = > {
    console.log("re-render Change");
    return (
      <div>
        <button
          onClick={()= >setTheme((v) => (v === "light" ? "Dark" : "light"))} > Change theme</button>
      </div>
    );
  }, [setTheme]);

  return dom;
}

function Other() {
  console.log("Other render, actually I should not re-render!!");
  return <div>Other, be reasonable, I shouldn't have done it!</div>;
}
Copy the code

Solving problem 1

The ThemeContext is pulled out and the child component is passed in via the children property of props. Children will not change even if themecontext.provider is re-rendered. This will not cause all child components to be re-rendered because of a value change.

Problem Solving 2

The above method can be used to solve the problem of repeated overall rendering, but the problem of local rendering is more complicated. We need to use useMemo to modify the sub-components one by one, or use React. Memo to refine the sub-components.

reference

UseContext adds react usecontext_React performance optimization to useMemo