createContext 你用對了嗎?
前言
createContext
是 react 提供的用於全局狀態管理的一個 api,我們可以通過Provider
組件注入狀態,用Consumer
組件或者useContext
api 獲取狀態(推薦使用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
是根據發布訂閱模式來實現的,Provider
的value
值每次發生變化都會通知所有使用它的組件(使用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
抽離出來,子組件通過props
的children
屬性傳遞進來。即使ThemeContext.Provider
重新渲染,children
也不會改變。這樣就不會因為value
值改變導致所有子組件跟著重新渲染了。
解決問題2
通過上面的方式可以一刀切的解決整體重複渲染的問題,但局部渲染的問題就比較繁瑣了,需要我們用useMemo
一個個的修改子組件,或者使用React.memo
把子組件更加細化。
參考
useContext深入學習
奇怪的useMemo知識增加了
react usecontext_React性能優化篇