概念16.3+

Context

在组件树中跨层级共享数据,避免 props 穿透

什么是 Context?

Context 提供了一种在组件树中跨层级传递数据的方式, 而不需要在每一层手动传递 props。

Props 穿透的问题

TypeScript
// ❌ Props 穿透:每一层都要传递
function App() {
  const user = { name: 'Taylor', age: 25 };
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Header user={user} />;
}

function Header({ user }) {
  return <UserAvatar user={user} />;
}

function UserAvatar({ user }) {
  return <img src={user.avatar} alt={user.name} />;
}

使用 Context 解决

TypeScript
// ✅ Context:直接传递给需要的组件
function App() {
  const user = { name: 'Taylor', age: 25 };
  return (
    <UserContext.Provider value={user}>
      <Layout />
    </UserContext.Provider>
  );
}

function Layout() {
  return <Header />; // 不需要传递 user
}

function Header() {
  return <UserAvatar />; // 不需要传递 user
}

function UserAvatar() {
  const user = useContext(UserContext); // 直接获取
  return <img src={user.avatar} alt={user.name} />;
}
使用场景

Context 适合用于全局跨多层级共享的数据:

  • 主题 (深色/浅色模式)
  • 用户认证信息
  • 语言/国际化设置
  • 应用配置

创建 Context

基本语法

TypeScript
import { createContext } from 'react';

// 创建 Context
const MyContext = createContext(defaultValue);

创建 Context 示例

TypeScript
import { createContext } from 'react';

// 创建主题 Context
const ThemeContext = createContext('light');

// 创建用户 Context
const UserContext = createContext(null);

默认值

默认值只在没有匹配的 Provider 时使用。 如果组件被 Provider 包裹,则使用 Provider 的值。

TypeScript
const ThemeContext = createContext('light');

function Button() {
  // 如果没有 Provider,theme 为 'light'
  const theme = useContext(ThemeContext);
  return <button className={theme}>点击</button>;
}

// 使用
<Button /> // theme = 'light' (默认值)

<ThemeContext.Provider value="dark">
  <Button /> // theme = 'dark' (Provider 的值)
</ThemeContext.Provider>
命名约定

通常,Context 文件名和变量名使用 PascalCase, 例如 ThemeContextUserContext

提供 Context

Provider 组件

TypeScript
import { createContext, useState } from 'react';

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

提供动态值

TypeScript
function App() {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Page />
    </ThemeContext.Provider>
  );
}

嵌套 Provider

TypeScript
function App() {
  const user = { name: 'Taylor' };
  const theme = 'dark';

  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Page />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// 内层的 Provider 会覆盖外层
<ThemeContext.Provider value="light">
  <ThemeContext.Provider value="dark">
    <Button /> {/* theme = 'dark' */}
  </ThemeContext.Provider>
</ThemeContext.Provider>

分离 Provider

TypeScript
// context/ThemeContext.tsx
import { createContext, useState } from 'react';

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};

// 使用
// app.tsx
function App() {
  return (
    <ThemeProvider>
      <Page />
    </ThemeProvider>
  );
}

// components/Button.tsx
function Button() {
  const { theme, toggleTheme } = useTheme();
  return <button onClick={toggleTheme}>{theme}</button>;
}
注意

Provider 的 value 会变化。每次渲染时, 如果创建了新的对象或数组,所有使用该 Context 的组件都会重新渲染。

消费 Context

使用 useContext Hook

TypeScript
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>点击</button>;
}

使用 Consumer 组件

TypeScript
// 在类组件或不使用 Hook 时使用
function Button() {
  return (
    <ThemeContext.Consumer>
      {theme => <button className={theme}>点击</button>}
    </ThemeContext.Consumer>
  );
}

多个 Context

TypeScript
function Header() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);

  return (
    <header className={theme}>
      <h1>欢迎, {user.name}</h1>
    </header>
  );
}
Hook vs Consumer

useContext Hook 是更现代和推荐的方式。 Consumer 组件主要用于类组件或特殊场景。

性能优化

重要:Context 的性能影响

Context 的一个关键特性:当 Context 值改变时,所有消费该 Context 的组件都会重新渲染, 即便它们只使用了 Context 值的一部分。

这意味着:

  • Context 不适合频繁变化的值(如鼠标位置、输入框值)
  • 大型组件树中,Context 更新可能导致大量不必要的渲染
  • 应该将频繁变化和不常变化的数据拆分到不同的 Context 中

示例:如果 Context 包含 { user, theme, language },当 theme 改变时, 使用 user 的组件也会重新渲染!

问题:不必要的重新渲染

TypeScript
// 每次 theme 改变,所有消费者都会重新渲染
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={user}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

解决方案 1: 拆分 Context

TypeScript
// ✅ 拆分为独立的 Context
const ThemeStateContext = createContext(null);
const ThemeDispatchContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeStateContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={setTheme}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeStateContext.Provider>
  );
}

// 只需要 theme 的组件
function Title() {
  const theme = useContext(ThemeStateContext);
  return <h1 className={theme}>标题</h1>;
}

// 只需要 setTheme 的组件
function ThemeToggle() {
  const setTheme = useContext(ThemeDispatchContext);
  return <button onClick={() => setTheme('dark')}>切换</button>;
}

解决方案 2: 使用 useMemo(有限优化)

TypeScript
function App() {
  const [theme, setTheme] = useState('light');

  // ⚠️ 注意:这个优化作用有限
  // 因为 setTheme 是稳定的,但 theme 每次改变都会创建新的 value 对象
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      <Page />
    </ThemeContext.Provider>
  );
}
useMemo 的局限性

使用 useMemo 只能避免不必要的对象创建,但当 theme 改变时, 所有使用该 Context 的组件仍然会重新渲染。最有效的优化方案是方案 1(拆分 Context)。

解决方案 3: 选择性订阅

TypeScript
// 创建一个只订阅需要数据的组件
function ThemedTitle({ title }) {
  const theme = useContext(ThemeContext);
  return <h1 className={theme}>{title}</h1>;
}

// 父组件传递其他 props
function Page() {
  const title = "标题";
  return <ThemedTitle title={title} />;
}
优化原则

分离读写是一种常见的优化模式。 将 state 和 dispatch 放在不同的 Context 中, 可以减少不必要的重新渲染。

常见模式

1. 主题切换

TypeScript
const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

2. 用户认证

TypeScript
const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkAuth().then(setUser).finally(() => setLoading(false));
  }, []);

  const login = async (credentials) => {
    const user = await loginAPI(credentials);
    setUser(user);
  };

  const logout = async () => {
    await logoutAPI();
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

3. 国际化

TypeScript
const I18nContext = createContext(null);

export function I18nProvider({ children, locale = 'zh-CN' }) {
  const [currentLocale, setLocale] = useState(locale);
  const [messages, setMessages] = useState(translations[locale]);

  const changeLocale = (newLocale) => {
    setLocale(newLocale);
    setMessages(translations[newLocale]);
  };

  const t = (key) => messages[key] || key;

  return (
    <I18nContext.Provider value={{ locale: currentLocale, t, changeLocale }}>
      {children}
    </I18nContext.Provider>
  );
}

4. 表单状态

TypeScript
const FormContext = createContext(null);

export function FormProvider({ children, initialValues, onSubmit }) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = (name) => (value) => {
    setValues(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = () => {
    onSubmit(values);
  };

  return (
    <FormContext.Provider
      value={{ values, errors, touched, handleChange, handleSubmit }}
    >
      {children}
    </FormContext.Provider>
  );
}
不要过度使用 Context

Context 不是状态管理工具。对于复杂的全局状态, 考虑使用 Redux、Zustand 或 Jotai 等状态管理库。

Context vs Props

特性PropsContext
数据流向单向,从父到子跨层级,任何组件都可以访问
可追踪性清晰,明确知道数据来源不太清晰,数据来源可能很远
适用场景组件间通信,UI 数据全局状态,主题,用户信息
性能好,只影响需要的组件可能导致不必要的重新渲染
类型检查容易,明确的 props 类型需要额外的工作

何时使用 Context?

使用 Context 当:

  • 数据需要在多个不相关的组件中共享
  • 数据需要在多层级组件树中传递
  • 数据是“全局”的(如主题、用户、语言)

何时使用 Props?

使用 Props 当:

  • 数据只在父子组件间传递
  • 组件的可复用性很重要
  • 想要明确的数据流向
判断标准

如果你发现自己通过多层组件传递相同的 props, 那么可能需要考虑使用 Context。

TypeScript 支持

定义 Context 类型

TypeScript
interface Theme {
  mode: 'light' | 'dark';
  primaryColor: string;
}

interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

创建自定义 Hook

TypeScript
export function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }

  return context;
}

使用

TypeScript
function Button() {
  const { theme, setTheme } = useTheme();

  return (
    <button style={{ color: theme.primaryColor }}>
      点击
    </button>
  );
}

测试 Context

包裹 Provider

TypeScript
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { Button } from './Button';

test('renders button with theme', () => {
  render(
    <ThemeProvider value={{ theme: 'dark' }}>
      <Button />
    </ThemeProvider>
  );

  expect(screen.getByRole('button')).toHaveClass('dark');
});

创建测试工具

TypeScript
// test-utils.tsx
import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';

export function renderWithProviders(
  ui,
  { theme = 'light', ...renderOptions } = {}
) {
  function Wrapper({ children }) {
    return <ThemeProvider value={{ theme }}>{children}</ThemeProvider>;
  }

  return render(ui, { wrapper: Wrapper, ...renderOptions });
}

// 使用
test('renders button', () => {
  renderWithProviders(<Button />, { theme: 'dark' });
});

最佳实践

1. 创建自定义 Hook

TypeScript
// ✅ 好:封装 Context 使用
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 使用
function Component() {
  const { theme } = useTheme();
  return <div>{theme}</div>;
}

2. 分离 state 和 dispatch

TypeScript
// ✅ 好:拆分 Context
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>
  );
}

3. 导出 Context 类型

TypeScript
// ✅ 好:导出类型
export const ThemeContext = createContext<ThemeType>(null);
export const useTheme = (): ThemeType => useContext(ThemeContext);

4. 提供默认值或检查

TypeScript
// ✅ 好:使用检查
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
};
避免的陷阱
  • ❌ 在 Context 中存储太多状态
  • ❌ 频繁更新 Context 值(导致性能问题)
  • ❌ 忘记优化 Context Provider 的 value
  • ❌ 混淆 Context 和状态管理

常见问题

Context 值没有更新?

确保你在 Provider 中更新了 value,并且组件使用了 useContext。

所有组件都重新渲染了?

考虑拆分 Context 或使用 useMemo 优化 value。

如何在服务端渲染中使用?

TypeScript
// 使用静态值或 Server Actions
function App() {
  return (
    <ThemeProvider theme="light">
      <Page />
    </ThemeProvider>
  );
}

如何处理多个 Context?

TypeScript
// 组合多个 Provider
function Providers({ children }) {
  return (
    <ThemeProvider>
      <UserProvider>
        <I18nProvider>
          {children}
        </I18nProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

相关概念

这篇文章有帮助吗?