Context在项目中应用的最大问题是全局更新------在顶层修改Context的value,会导致整个组件树都会重新渲染。
使用memo防止额外渲染
如下代码中,Doo组件并没有依赖任何context:
typescript
const context = createContext({ count1: 0, count2: 0 });
function Foo() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const add1 = useCallback(() => {
setCount1((value) => ++value);
}, []);
const add2 = useCallback(() => {
setCount2((value) => ++value);
}, []);
return (
<context.Provider value={{ count1: count1, count2: count2 }}>
<div>
<button
className="border rounded-full px-2 bg-blue-400 text-white"
onClick={add1}
>
add count1
</button>
<button
className="border rounded-full px-2 bg-blue-400 text-white"
onClick={add2}
>
add count2
</button>
<Boo></Boo>
<Coo></Coo>
<Doo></Doo>
</div>
</context.Provider>
);
}
function Boo() {
const { count1 } = useContext(context);
console.log("render boo");
return <div>boo:使用context.count1:{count1}</div>;
}
function Doo() {
console.log("render doo");
return <div>doo:未使用context</div>;
}
function Coo() {
const { count2 } = useContext(context);
console.log("render coo");
return <div>coo:使用context.count2:{count2}</div>;
}
点击按钮后Doo仍然会重新渲染:

这一点很好理解------父级的setState会触发发子组件的render。因此只要对子组件增加memo就可以阻止该次render。
diff
+const MemoDoo = memo(Doo);
function Foo() {
...
return (
<context.Provider value={{ count1: count1, count2: count2 }}>
<div>
...
- <Doo></Doo>
+ <MemoDoo></MemoDoo>
</div>
</context.Provider>
);
}
这样Doo组件确实没有额外更新了,但是Boo和Coo,两个组件分别依赖context中某个值,该值即使没有变也会更新。这种情况即便添加了memo,也无法阻止额外更新。

这一点也不难理解,useContext并不会关心你到底用到了context中哪个值,只要context更新,那么组件就会更新。
因此想要解决这个问题,就得确保context不变。这一点似乎又和全局状态共享冲突:如何一方面保持context不变,另一方面又能接收到最新的状态值呢?
useSelector和useSyncExternalStore
上文提到需要保持context不变,又要响应状态更新,因此状态要脱离react的管理,context中存放状态存取的方法,再用useSyncExternalStore去响应外部的状态更新,就可以完美实现以上诉求。
jsx
// ============= 创建 Store =============
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
setState: (newState) => {
state = typeof newState === "function" ? newState(state) : newState;
listeners.forEach((l) => l());
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
// ============= Context Provider =============
const StoreContext = createContext(null);
export function StoreProvider({ children, initialState }) {
const store = useMemo(() => createStore(initialState), []);
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}
// ============= useSelector 实现 =============
export function useSelector(selector) {
const store = useContext(StoreContext);
if (!store) throw new Error("useSelector must be used within StoreProvider");
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getState())
);
}
// ============= 派发方法 =============
export function useStore() {
const store = useContext(StoreContext);
if (!store) throw new Error("useStore must be used within StoreProvider");
return store.setState;
}
function UserInfo() {
const user = useSelector((s) => s.user);
console.log("UserInfo render");
return <div>👤 {user.name}</div>;
}
function ThemeInfo() {
const theme = useSelector((s) => s.theme);
console.log("ThemeInfo render");
return <div>🎨 {theme}</div>;
}
function Test2() {
const initialState = { user: { name: "Alice" }, theme: "light" };
return (
<StoreProvider initialState={initialState}>
<Toolbar />
</StoreProvider>
);
}
function Toolbar() {
const setState = useStore();
const toggleTheme = () =>
setState((s) => ({ ...s, theme: s.theme === "light" ? "dark" : "light" }));
const renameUser = () =>
setState((s) => ({ ...s, user: { name: s.user.name + "!" } }));
return (
<>
<UserInfo />
<ThemeInfo />
<button
className="border rounded-full px-2 bg-blue-400 text-white"
onClick={toggleTheme}
>
Toggle Theme
</button>
<button
className="border rounded-full px-2 bg-blue-400 text-white"
onClick={renameUser}
>
Rename User
</button>
</>
);
}
效果如下------状态变更后,两个组件分别更新:

其他思路
如果是react 16,无法使用useSyncExternalStore,还有其他办法吗?有的。
参考Redux提供connect函数,该函数接收一个组件和一个selector,内部再对该组件包裹一个context.Provider,value是selector后的值,就可以实现按需更新。
总结
- 组件使用useContext后,memo无法阻止不必要的更新
- 不建议使用Context作状态管理,但可以作全局静态数据的共享
- 简单项目可以使用Context + useSyncExternalStore自行封装状态库,不需要依赖第三方库
- 如果自行维护状态管理太麻烦,建议使用redux或zustand等状态库