Did you use createContext correctly?

Posted by fotofx on Sat, 30 Oct 2021 06:44:35 +0200

catalogue

preface

createContext is an api provided by react for global state management. We can inject state through the Provider component and use the Consumer component or useContext api   Obtain the status (useContext is recommended, which is more concise).

createContext makes the communication between components more convenient, but it will bring great performance problems if it is not used properly. Next, we will discuss the causes of performance problems and how to optimize them.

Root causes of performance problems

Let's take an example: createContext performance problem reason , notice the two problem points in the 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 causes:
   * ThemeCtx.Provider Parent component rendering causes all child components to render with it
   */

  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: no changed value in the value state causes the component to render
  console.log("setTheme No change, in fact, I should not render!!!");
  return (
    <div>
      <button
        onClick={() => setTheme((v) => (v === "light" ? "dark" : "light"))}
      >
        change theme
      </button>
    </div>
  );
}

function Other() {
  // Problem 1: rendering of subcomponents independent of value state
  console.log("Other render. In fact, I should not re render!!!");
  return <div>other Components, make sense, I shouldn't render!</div>;
}

Question 1 (overall repeated rendering): all the sub components wrapped by the Provider component are rendered

It can be seen from this example that using ThemeCtx.Provider to directly wrap sub components will cause all sub components to be re rendered each time the ThemeCtx.Provider component is rendered. The reason is that for the component created using React.createElement(type, props: {},...), props: {} will be a new object each time.

Problem 2 (local duplicate rendering): using useContext causes the component to render

createContext is implemented according to the publish subscribe mode. Every time the value value of the Provider changes, it will notify all components using it (components using useContext) to re render.

Solution

We have analyzed the root cause of the problem above, and now we begin to solve the problem. Let's also 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,In fact, I should not re render!!!");
  return <div>other,Make sense, I shouldn't play up!</div>;
}

Problem solving 1

Extract the ThemeContext, and the child components are passed in through the children attribute of props. Children will not change even if ThemeContext.Provider re renders. In this way, all sub components will not be re rendered due to the change of value value.

Problem solving 2

The above method can solve the problem of overall repeated rendering, but the problem of local rendering is more cumbersome. We need to modify the sub components one by one with useMemo, or refine the sub components with React.memo.

reference resources

useContext in-depth learning
Strange useMemo knowledge has increased
react usecontext_React performance optimization

Topics: React