React Context 深入理解
Context 的设计动机
React 的组件模型是单向数据流:数据从父组件通过 props 向下传递。当组件层级很深时,会出现「prop drilling」问题——中间层的组件虽然不需要这些数据,但不得不转发它们。
// prop drilling 示例
function App() {
const [user, setUser] = useState({ name: 'Alice', theme: 'dark' });
return (
<Page user={user} theme={theme} /> // 需要传递 theme
);
}
function Page({ user, theme }) {
return (
<Layout theme={theme}> // 需要传递 theme
<Sidebar theme={theme} /> // 需要传递 theme
<Content user={user} theme={theme} /> // 实际需要 theme 的组件
</Layout>
);
}
function Content({ user, theme }) {
// 终于用到 theme 了
return <div className={theme}>{user.name}</div>;
}
Context 解决了这个问题——它允许父组件向整个子树广播数据,而不需要每一层都显式传递。
Context 的基础 API
创建 Context
import { createContext } from 'react';
const ThemeContext = createContext({
primary: 'blue',
secondary: 'gray',
});
// createContext 的参数是默认值
// 只有在找不到 Provider 时才使用
提供 Context
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
消费 Context
函数组件有两种方式:
// 方式一:useContext Hook
function Content() {
const theme = useContext(ThemeContext);
return <div className={theme}>Content</div>;
}
// 方式二:Class 组件的 Context Consumer
class Content extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{value => <div className={value}>{this.props.children}</div>}
</ThemeContext.Consumer>
);
}
}
Context 的工作机制
上下文值的存储结构
Context 不仅仅是简单的发布-订阅,它有更复杂的内部结构:
// React 内部创建的 Context 对象
const Context = {
$$typeof: Symbol('react.context'),
Provider: { /* ... */ },
Consumer: { /* ... */ },
};
// Provider 内部会创建一个 fiber 节点
// 存储在 fiber.memoizedProps.context 值中
Provider 的 value 传递
<ThemeContext.Provider value={theme}>
<DeepTree />
</ThemeContext.Provider>
value 存储在 Provider 组件的 fiber 节点上。当 Context 值变化时,React 会标记所有消费该 Context 的组件需要重新渲染。
useContext 的工作原理
useContext 的实现大致是:
function useContext(Context) {
const dispatcher = ReactCurrentDispatcher.current;
// 从当前 fiber 向上查找匹配的 Provider
const fiber = resolveCurrentlyRenderingFiber();
if (fiber) {
const value = findContextProvider(fiber, Context);
return value;
}
// 如果找不到 Provider,返回默认值
return Context._defaultValue;
}
Context 的性能问题
问题根源:值变化导致所有消费者更新
function App() {
const [user, setUser] = useState({ name: 'Alice' });
const [theme, setTheme] = useState('dark');
// 问题:这个对象每次渲染都是新引用
const value = { user, theme };
return (
<ThemeContext.Provider value={value}>
<Page />
</ThemeContext.Provider>
);
}
即使 user 或 theme 的内容没变,只要引用变了,所有消费者都会重新渲染。
解决方案一:拆分 Context
// 不要把所有状态放一个 Context
const AppContext = createContext({ user: null, theme: null });
// 拆分成多个 Context
const UserContext = createContext(null);
const ThemeContext = createContext('light');
// 独立更新,互不影响
function App() {
const [user, setUser] = useState({ name: 'Alice' });
const [theme, setTheme] = useState('dark');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
解决方案二:使用 useMemo 稳定引用
function App() {
const [user, setUser] = useState({ name: 'Alice' });
const [theme, setTheme] = useState('dark');
// 只有 user 变化时才创建新对象
const userValue = useMemo(() => ({ user }), [user]);
// 只有 theme 变化时才创建新字符串
const themeValue = useMemo(() => theme, [theme]);
return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<Page />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
解决方案三:状态与 Dispatch 分离
// 推荐模式:Context 只传递 dispatch
const CountContext = createContext(null);
function CountProvider({ children }) {
const [count, setCount] = useState(0);
const dispatch = useCallback((action) => {
setCount(prev => reducer(prev, action));
}, []);
return (
<CountContext.Provider value={dispatch}>
{children}
</CountContext.Provider>
);
}
function Counter() {
const dispatch = useContext(CountContext);
// dispatch 函数永远是稳定的,不需要是 Context 的值
return <button onClick={() => dispatch({ type: 'increment' })}>+</button>;
}
Context 与渲染的关系
Context 如何触发重新渲染
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
当 Provider 的 value 变化时:
- React 标记所有直接消费者需要重新渲染
- 如果消费者是
PureComponent或使用了memo,会比较 props - 即使组件没有直接使用变化的值,只要它消费了这个 Context,就会被重新渲染
消费 Context 的组件不一定重新渲染
function Button() {
const theme = useContext(ThemeContext);
// Button 消费了 ThemeContext
// 但如果 Button 被 memo 了,并且 theme 没变化,就不会重新渲染
return <button className={theme}>Click</button>;
}
const MemoizedButton = memo(Button);
中间组件对渲染的影响
function Middle() {
const [count, setCount] = useState(0);
// 这个组件消费了 ThemeContext
const theme = useContext(ThemeContext);
return (
<div>
<MemoizedChild /> {/* 如果 MemoizedChild 不消费 Context,可能不会重新渲染 */}
</div>
);
}
中间组件即使消费了 Context,只要它被 memo 包裹且 props 没变,就不会导致子组件重新渲染。
Context 的最佳实践
模式一:Context + Reducer
// 分离 state 和 dispatch
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
export function useState() {
return useContext(StateContext);
}
export function useDispatch() {
return useContext(DispatchContext);
}
模式二:自定义 Hook 封装
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
模式三:Context Selector
function useContextSelector(Context, selector) {
const contextValue = useContext(Context);
const selectedValue = useMemo(
() => selector(contextValue),
[contextValue, selector]
);
return selectedValue;
}
// 使用
function Button() {
// 只订阅 theme,不订阅 setTheme
const theme = useContextSelector(
ThemeContext,
value => value.theme
);
return <button className={theme}>Click</button>;
}
Context 的适用场景
适合使用 Context 的场景
- 主题:暗色/亮色模式,全局样式
- 用户认证状态:当前登录用户信息
- 语言/国际化:当前语言设置
- 全局配置:API 基础 URL、功能开关
- 路由状态:当前路由、历史记录
不适合使用 Context 的场景
- 频繁变化的数据:如表单输入,每次 keystroke 都变化
- 大量数据的部分更新:如列表中的单个项目变化
- 需要细粒度更新的复杂状态:考虑用状态管理库
面试中的表达
面试中聊到 Context,通常是在考察你对 React 数据流和性能优化的理解:
Context 的设计初衷是避免 prop drilling,而不是作为状态管理方案。它的本质是广播机制——当 Provider 的 value 变化时,所有消费该 Context 的组件都会收到通知。
Context 的主要性能问题是:value 的任何变化(即使是引用变化)都会导致所有消费者重新渲染。解决方案是拆分 Context、分离 state 和 dispatch、使用 useMemo 稳定引用。
Context 不适合频繁变化的场景,因为每次变化都会触发整个子树的重渲染。如果需要管理复杂状态,考虑使用 Zustand、Jotai 或 Redux 等状态管理库,它们通常提供了更细粒度的更新机制。
延展阅读
- React Docs: Context — 官方 Context 文档
- React Docs: Scaling Up with Reducer and Context — Context 与 Reducer 结合
- Kent C. Dodds: Context is a module state pattern — Context 的正确使用姿势
- React Reconciliation: State Preservation — React 如何决定状态保留还是重置