createContext 你用對了嗎?

奔跑的瓜牛發表於2021-10-30

更好的體驗可前往掘金閱讀: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效能優化篇

相關文章