最近项目中一个React Context在不断的接受websocket事件,然后一直修改state,导致重复渲染过多,比较卡顿
TM的这状态管理真乱,趁机总结一下React Context的使用的注意事项
React Context 用于在组件树中传递数据,而不必手动地通过 props 逐层传递。然而,它的便利性也带来了一个常见的性能陷阱:当 Context 的值发生变化时,所有依赖该 Context 的消费组件都会重新渲染,即使它们只使用了 Context 值中的一小部分。
如果处理不当,这种全局性的重新渲染可能会拖慢你的应用,尤其是在 Context Provider 位于组件树顶层,并且其值包含频繁变动的数据时。
Context的重新渲染机制
当我们使用 useContext(MyContext) 或 <MyContext.Consumer> 时,React 会在内部建立一个订阅关系。
- 当 Provider 的
value属性发生变化时 :React 会检查新旧值是否严格相等(===)。 - 如果值不相等:React 会通知所有订阅了该 Context 的 Consumer 组件执行重新渲染。
React 并没有对 Consumer 实际使用了 Context 值中的哪部分属性 进行细粒度分析。只要 Context 的 value 对象本身 引用发生了变化,所有 Consumer 都会触发更新。
JavaScript
// ❌ 常见但易导致全局渲染的模式
const MyContext = createContext({ user: null, settings: {} });
function App() {
// state 只要更新,value 对象就会创建一个新的引用
const [appState, setAppState] = useState({ user: { name: 'Gemini' }, theme: 'dark' });
// 每次 App 渲染,这个对象都是一个新的引用
const contextValue = useMemo(() => appState, [appState]);
return (
<MyContext.Provider value={contextValue}>
<Header />
<Content />
<Footer />
</MyContext.Provider>
);
}
// 假设 Header 只使用了 appState.user
function Header() {
const { user } = useContext(MyContext);
// ... 其他代码
return <h1>Welcome, {user.name}</h1>;
}
// 假设 Footer 只使用了 appState.theme
function Footer() {
const { theme } = useContext(MyContext);
// ... 其他代码
return <p>Current theme: {theme}</p>;
}
// ⚡️ 陷阱:即使只有 theme 变化,Header 也会重新渲染!
避免全局重新渲染
我们可以通过以下几种策略,将 Context 的重新渲染范围限制在真正需要更新的组件。
1. 拆分 Context
这是最简单、最有效的策略之一。与其将所有状态都塞入一个"大 Context"中,不如根据数据的更新频率 和耦合关系将其拆分成多个独立的 Context。
- 高频更新 / 独立的 Context :例如,用户交互状态(
IsLoadingContext)。 - 低频更新 / 共享的 Context :例如,全局配置和静态数据(
ThemeContext)。
JavaScript
// ✅ 拆分成多个独立的 Context
const UserContext = createContext(null);
const ThemeContext = createContext(null);
function App() {
const [user, setUser] = useState({ name: 'Gemini' });
const [theme, setTheme] = useState('dark');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Header /> {/* 仅消费 UserContext */}
<Footer /> {/* 仅消费 ThemeContext */}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// 优化效果:
// 1. user 变化,只有 Header 及其子树可能重新渲染。
// 2. theme 变化,只有 Footer 及其子树可能重新渲染。互不影响。
2. 使用 Custom Hook 和 memo 结合
这种方法适用于你无法拆分 Context,但又想防止 Consumer 重新渲染的情况
通过 useMemo 或自定义 Hook 仅提取 Context 中需要的属性,并结合 React.memo 来跳过不必要的渲染
JavaScript
// 针对 Header 组件的自定义 Hook
const useUser = () => {
const context = useContext(MyContext);
// 仅返回 user 部分,确保只有 user 改变时才返回新引用
return useMemo(() => context.user, [context.user]);
};
// 结合 React.memo
const MemoizedHeader = React.memo(function Header() {
const user = useUser(); // 即使 MyContext 整体变了,只要 user 不变,useUser 就会返回旧引用
// ... 渲染逻辑
});
// ⚡️ 陷阱规避:
// 1. MyContext 整体改变,MemoizedHeader 接收新的 props (即 useUser 返回的值)。
// 2. 但由于 useUser() 对 user 属性进行了 useMemo 优化,如果 user 对象引用没有变化,
// 3. React.memo 就会发挥作用,跳过 Header 的渲染。
END
祝大家暴富!!!