createContext 你用對了嗎?

前言

createContext是 react 提供的用於全局狀態管理的一個 api,我們可以通過Provider組件注入狀態,用Consumer組件或者useContextapi 獲取狀態(推薦使用useContext方式,更加簡潔)。

createContext讓組件間的通訊更為方便,但如果使用不當卻會帶來很大的性能問題。下面我們會討論引起性能問題的原因以及如何優化。

性能問題的根源

先來看一個例子:createContext性能問題原因,注意例子中的2個問題點。

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

const ThemeCtx = createContext({});

export default function App() {
  const [theme, setTheme] = useState("dark");
  /**
   * 性能問題原因:
   * ThemeCtx.Provider 父組件渲染導致所有子組件跟著渲染
   */

  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);
  // 問題2:value 狀態中沒有改變的值導致組件渲染
  console.log("setTheme 沒有改變,其實我也不應該渲染的!!!");
  return (
    <div>
      <button
        onClick={() => setTheme((v) => (v === "light" ? "dark" : "light"))}
      >
        改變theme
      </button>
    </div>
  );
}

function Other() {
  // 問題1:和 value 狀態無關的子組件渲染
  console.log("Other render。其實我不應該重新渲染的!!!");
  return <div>other組件,講道理,我不應該渲染的!</div>;
}

問題1(整體重複渲染):Provider組件包裹的子組件全部渲染

從這個例子可以看出來,用ThemeCtx.Provider直接包裹子組件,每次ThemeCtx.Provider組件渲染會導致所有子組件跟著重新渲染,原因是使用React.createElement(type, props: {}, ...)創建的組件,每次props: {}都會是一個新的對象。

問題2(局部重複渲染):使用useContext導致組件渲染

createContext是根據發布訂閱模式來實現的,Providervalue值每次發生變化都會通知所有使用它的組件(使用useContext的組件)重新渲染。

解決方案

上面我們分析了問題的根源,下面就開始解決問題。 同樣先看一下優化後的例子:createContext性能優化

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);

  /**
   * 解決方案:使用 useMemo
   *
   */
  const dom = useMemo(() => {
    console.log("re-render Change");
    return (
      <div>
        <button
          onClick={() => setTheme((v) => (v === "light" ? "dark" : "light"))}
        >
          改變theme
        </button>
      </div>
    );
  }, [setTheme]);

  return dom;
}

function Other() {
  console.log("Other render,其實我不應該重新渲染的!!!");
  return <div>other,講道理,我不應該渲染的!</div>;
}

解決問題1

ThemeContext抽離出來,子組件通過propschildren屬性傳遞進來。即使ThemeContext.Provider重新渲染,children也不會改變。這樣就不會因為value值改變導致所有子組件跟著重新渲染了。

解決問題2

通過上面的方式可以一刀切的解決整體重複渲染的問題,但局部渲染的問題就比較繁瑣了,需要我們用useMemo一個個的修改子組件,或者使用React.memo把子組件更加細化。

參考

useContext深入學習
奇怪的useMemo知識增加了
react usecontext_React性能優化篇

Tags: