React Context 深入理解

深入理解 React Context 的工作机制、性能陷阱、以及如何通过拆分 Context 和选择器模式来优化 Context 的使用。

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>
  );
}

即使 usertheme 的内容没变,只要引用变了,所有消费者都会重新渲染。

解决方案一:拆分 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 变化时:

  1. React 标记所有直接消费者需要重新渲染
  2. 如果消费者是 PureComponent 或使用了 memo,会比较 props
  3. 即使组件没有直接使用变化的值,只要它消费了这个 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 的场景

  1. 主题:暗色/亮色模式,全局样式
  2. 用户认证状态:当前登录用户信息
  3. 语言/国际化:当前语言设置
  4. 全局配置:API 基础 URL、功能开关
  5. 路由状态:当前路由、历史记录

不适合使用 Context 的场景

  1. 频繁变化的数据:如表单输入,每次 keystroke 都变化
  2. 大量数据的部分更新:如列表中的单个项目变化
  3. 需要细粒度更新的复杂状态:考虑用状态管理库

面试中的表达

面试中聊到 Context,通常是在考察你对 React 数据流和性能优化的理解:

Context 的设计初衷是避免 prop drilling,而不是作为状态管理方案。它的本质是广播机制——当 Provider 的 value 变化时,所有消费该 Context 的组件都会收到通知。

Context 的主要性能问题是:value 的任何变化(即使是引用变化)都会导致所有消费者重新渲染。解决方案是拆分 Context、分离 state 和 dispatch、使用 useMemo 稳定引用。

Context 不适合频繁变化的场景,因为每次变化都会触发整个子树的重渲染。如果需要管理复杂状态,考虑使用 Zustand、Jotai 或 Redux 等状态管理库,它们通常提供了更细粒度的更新机制。


延展阅读