Context 真的是 React 里“又香又容易翻车”的东西:不想层层传 props?用它。结果某个值一变,凡是 useContext 过的组件全都跟着 re-render,一不小心就把页面拖慢。

我这篇想解决的不是“能不能用 Context”,而是“用了以后怎么别把整棵树一起带飞”。下面按一个比较工程化的思路:先搞清楚它为什么会触发渲染,再给出几种最常用、最容易落地的缩小影响范围的方法。

理解 Context 更新导致的重新渲染

你可以把 Context 理解成“全局订阅”:组件一旦 useContext(SomeContext),就相当于订阅了这个 Context 的 value。只要 Provider 的 value 变了,这些订阅者都会重新渲染。

关键点在于:React 比较的是 value 的引用是否变化。也就是说,哪怕里面的字段没变,只要你每次 render 都 new 一个对象/数组/函数,引用就变了,订阅者照样会刷新。

优化策略

1. 使用 useMemo 缓存 Provider value

这是最常见也最“性价比高”的一招:让 Provider 的 value 引用稳定。别在 render 里直接写 value={{...}},更别把一堆临时函数塞进去——那等于告诉 React:“每次都当成更新处理”。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { createContext, useMemo, useState } from 'react';

export const MyContext = createContext();

const MyProvider = ({ children }) => {
const [state, setState] = useState(0);

// 使用 useMemo 缓存 value 对象
const contextValue = useMemo(() => ({ state, setState }), [state]);

return (
<MyContext.Provider value={contextValue}>
{children}
</MyContext.Provider>
);
};

export default MyProvider;

这样只有当 state 真的变化时才会生成新对象,订阅者也就不会因为“引用变化但内容没变”而白跑一遍。

2. 拆分 Context

如果你把“用户信息 + 主题 + 权限 + 埋点开关 + …… ”都塞进一个 Context,那任何一个字段变化,所有消费者都会刷新。更合理的做法是按“关注点/更新频率”拆开:谁关心谁订阅,别互相误伤。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
export const UserContext = createContext();
export const ThemeContext = createContext();

const App = () => {
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<ChildComponent />
</ThemeContext.Provider>
</UserContext.Provider>
);
};

这样更新用户信息时,只会影响依赖 UserContext 的组件,主题相关组件不会重新渲染。

3. 使用 React.memo 进行组件优化

这里容易踩坑:React.memo 挡不住 Context 更新。组件只要读了 Context,Context 一变它还是会 re-render。memo 主要解决的是“父组件 re-render 导致子组件跟着 re-render”的问题——也就是 props 没变就别刷新。

所以你可以把它当成“配套措施”:当你的子组件既依赖 props、又依赖 context 时,memo 至少能把 props 这一侧的噪音压下去。

示例代码:

1
2
3
4
5
6
7
8
9
import React, { useContext } from 'react';
import { MyContext } from './MyProvider';

const ChildComponent = React.memo(() => {
const { state } = useContext(MyContext);
return <div>State: {state}</div>;
});

export default ChildComponent;

React.memo 会对 props 做浅比较,避免父组件 re-render 时带来的无意义刷新(注意:Context 更新仍然会触发)。

4. 使用 use-context-selector

如果你遇到的问题是“Context 里东西多、拆起来又很别扭”,那 use-context-selector 这种选择器方案会更细:消费者可以只订阅 Context value 的某个字段,其他字段变化不会影响它。

示例代码:

1
2
3
4
5
6
7
8
9
10
import { createContext } from 'react';
import { useContextSelector } from 'use-context-selector';

export const MyContext = createContext({ state: 0, setState: () => {} });

const ChildComponent = () => {
// 仅选择 state,而忽略 setState
const state = useContextSelector(MyContext, (ctx) => ctx.state);
return <div>State: {state}</div>;
};

这种方式的本质是把“订阅粒度”从整个对象降到你选出来的那一小块,对大型应用会很友好。


总结

  • 先把引用稳住:Provider 的 valueuseMemo 包一下,能省掉一堆“引用变了但内容没变”的刷新。
  • 按关注点拆 Context:别把所有状态塞一个大桶里,更新范围会被你自己放大。
  • memo 别用错地方:它挡不住 Context 更新,但能减少来自 props 的连坐渲染。
  • 需要更细就上 selectoruse-context-selector 让订阅粒度更小,适合 Context value 很臃肿的场景。

总之:Context 不是不能用,关键是别让它成为“一个字段更新,全家一起跑”的触发器。

Happy Coding!