Context
在组件树中跨层级共享数据,避免 props 穿透
什么是 Context?
Context 提供了一种在组件树中跨层级传递数据的方式, 而不需要在每一层手动传递 props。
Props 穿透的问题
// ❌ 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 解决
// ✅ 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
基本语法
import { createContext } from 'react';
// 创建 Context
const MyContext = createContext(defaultValue);
创建 Context 示例
import { createContext } from 'react';
// 创建主题 Context
const ThemeContext = createContext('light');
// 创建用户 Context
const UserContext = createContext(null);
默认值
默认值只在没有匹配的 Provider 时使用。 如果组件被 Provider 包裹,则使用 Provider 的值。
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,
例如 ThemeContext、UserContext。
提供 Context
Provider 组件
import { createContext, useState } from 'react';
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
提供动态值
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
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
// 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
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>点击</button>;
}
使用 Consumer 组件
// 在类组件或不使用 Hook 时使用
function Button() {
return (
<ThemeContext.Consumer>
{theme => <button className={theme}>点击</button>}
</ThemeContext.Consumer>
);
}
多个 Context
function Header() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
return (
<header className={theme}>
<h1>欢迎, {user.name}</h1>
</header>
);
}
useContext Hook 是更现代和推荐的方式。
Consumer 组件主要用于类组件或特殊场景。
性能优化
Context 的一个关键特性:当 Context 值改变时,所有消费该 Context 的组件都会重新渲染, 即便它们只使用了 Context 值的一部分。
这意味着:
- Context 不适合频繁变化的值(如鼠标位置、输入框值)
- 大型组件树中,Context 更新可能导致大量不必要的渲染
- 应该将频繁变化和不常变化的数据拆分到不同的 Context 中
示例:如果 Context 包含 { user, theme, language },当 theme 改变时,
使用 user 的组件也会重新渲染!
问题:不必要的重新渲染
// 每次 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
// ✅ 拆分为独立的 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(有限优化)
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 只能避免不必要的对象创建,但当 theme 改变时,
所有使用该 Context 的组件仍然会重新渲染。最有效的优化方案是方案 1(拆分 Context)。
解决方案 3: 选择性订阅
// 创建一个只订阅需要数据的组件
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. 主题切换
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. 用户认证
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. 国际化
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. 表单状态
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 不是状态管理工具。对于复杂的全局状态, 考虑使用 Redux、Zustand 或 Jotai 等状态管理库。
Context vs Props
| 特性 | Props | Context |
|---|---|---|
| 数据流向 | 单向,从父到子 | 跨层级,任何组件都可以访问 |
| 可追踪性 | 清晰,明确知道数据来源 | 不太清晰,数据来源可能很远 |
| 适用场景 | 组件间通信,UI 数据 | 全局状态,主题,用户信息 |
| 性能 | 好,只影响需要的组件 | 可能导致不必要的重新渲染 |
| 类型检查 | 容易,明确的 props 类型 | 需要额外的工作 |
何时使用 Context?
使用 Context 当:
- 数据需要在多个不相关的组件中共享
- 数据需要在多层级组件树中传递
- 数据是“全局”的(如主题、用户、语言)
何时使用 Props?
使用 Props 当:
- 数据只在父子组件间传递
- 组件的可复用性很重要
- 想要明确的数据流向
如果你发现自己通过多层组件传递相同的 props, 那么可能需要考虑使用 Context。
TypeScript 支持
定义 Context 类型
interface Theme {
mode: 'light' | 'dark';
primaryColor: string;
}
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
创建自定义 Hook
export function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
使用
function Button() {
const { theme, setTheme } = useTheme();
return (
<button style={{ color: theme.primaryColor }}>
点击
</button>
);
}
测试 Context
包裹 Provider
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');
});
创建测试工具
// 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
// ✅ 好:封装 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
// ✅ 好:拆分 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 类型
// ✅ 好:导出类型
export const ThemeContext = createContext<ThemeType>(null);
export const useTheme = (): ThemeType => useContext(ThemeContext);
4. 提供默认值或检查
// ✅ 好:使用检查
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。
如何在服务端渲染中使用?
// 使用静态值或 Server Actions
function App() {
return (
<ThemeProvider theme="light">
<Page />
</ThemeProvider>
);
}
如何处理多个 Context?
// 组合多个 Provider
function Providers({ children }) {
return (
<ThemeProvider>
<UserProvider>
<I18nProvider>
{children}
</I18nProvider>
</UserProvider>
</ThemeProvider>
);
}
相关概念
- Props - 组件间通信
- State - 组件状态
- useContext - Context Hook
- useReducer - 复杂状态管理