useContext
读取和订阅 Context,实现跨组件状态共享
核心概述
在 React 中,数据通常通过 props 自上而下传递。但当深层组件需要顶层组件的数据时, 会导致Props 穿透(Prop Drilling)问题:中间层组件被迫接收并转发它们自己不使用的 props。 这不仅让代码冗长,还让组件耦合度变高,难以维护。
useContext 通过 Context 机制来解决这个问题。 它允许组件"跨越"中间层,直接订阅顶层组件提供的数据。当 context 值变化时, 所有订阅该 context 的组件都会自动重新渲染。
useContext 适用于以下场景:
- 避免 Props 穿透:深层组件需要访问全局或共享状态(如主题、用户信息)
- 跨组件通信:不同层级的组件需要共享和更新同一份数据
- 全局状态管理:配合 useReducer 实现轻量级状态管理(主题、国际化、认证状态)
- 依赖注入:向组件树提供配置、服务实例或第三方库集成
💡 心智模型:隐形通道
将 Context 想象成组件树中的一条隐形通道:
- Provider: 在组件树入口处"铺设"通道,放入数据
- useContext: 组件通过"入口"进入通道,直接读取数据
- 订阅机制: 当通道中的数据更新时,所有入口处都会收到通知
不同于 props 需要一层层传递,Context 让数据可以"直达"目标组件。 但要注意:通道中的数据变化会通知所有订阅者,导致它们全部重新渲染。
技术规格
类型签名
function useContext<T>(context: Context<T>): T参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
context | Context<T> | 由 createContext 创建的 context 对象 |
返回值
useContext 返回该 context 的当前值。这个值由以下规则决定:
- 查找调用组件树中最近的
Context.Provider - 返回该 Provider 的
valueprop - 如果没有找到 Provider,返回
createContext(defaultValue)的默认值
运行机制
查找规则:
useContext会向上遍历组件树,查找最近的匹配 Provider- 查找范围仅限于调用组件的 Provider,不会跨越组件边界
- Provider 可以嵌套,内层 Provider 会覆盖外层 Provider 的值
订阅和更新:
- React 自动订阅 context 值的变化
- 当 Provider 的 value 变化时,所有使用该 context 的组件都会重新渲染
- 使用
Object.is比较新旧值,决定是否触发更新
性能影响:
- 即使组件使用了
React.memo,context 值变化仍会导致重新渲染 - 无法通过跳过子组件渲染来优化 context 更新
- 推荐将变化频繁的状态和不变的状态拆分为不同的 context
实战演练
基础用法:主题切换
最常见的 Context 使用场景是提供全局主题配置。
import { createContext, useContext } from 'react';
// ============= 1. 定义 Context 类型 =============
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
// ============= 2. 创建 Context =============
const ThemeContext = createContext<ThemeContextType | null>(null);
// ============= 3. 创建自定义 Hook =============
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// ============= 4. 创建 Provider 组件 =============
import { useState } from 'react';
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
}
export function ThemeProvider({ children, defaultTheme = 'light' }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const value: ThemeContextType = {
theme,
setTheme,
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// ============= 5. 使用 Context 的组件 =============
function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="px-4 py-2 rounded"
>
当前主题: {theme}
</button>
);
}
function ThemedCard() {
const { theme } = useTheme();
return (
<div className={`p-4 rounded ${theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-black'}`}>
<h3>卡片内容</h3>
<p>当前主题: {theme}</p>
</div>
);
}
// ============= 6. 应用入口 =============
export default function App() {
return (
<ThemeProvider defaultTheme="light">
<div className="min-h-screen p-8">
<ThemeSwitcher />
<ThemedCard />
</div>
</ThemeProvider>
);
}生产级案例:全局状态管理(useContext + useReducer)
展示如何使用 Context + useReducer 实现完整的全局状态管理,包括用户认证、主题、国际化等。
import { createContext, useContext, useReducer, ReactNode, useMemo } from 'react';
// ============= 类型定义 =============
interface User {
id: string;
name: string;
email: string;
}
interface AppState {
user: User | null;
theme: 'light' | 'dark';
locale: 'zh-CN' | 'en';
isLoading: boolean;
}
type AppAction =
| { type: 'SET_USER'; payload: User | null }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'SET_LOCALE'; payload: 'zh-CN' | 'en' }
| { type: 'SET_LOADING'; payload: boolean };
interface AppContextType {
state: AppState;
dispatch: React.Dispatch<AppAction>;
}
// ============= 创建 Context =============
const AppContext = createContext<AppContextType | null>(null);
// ============= Reducer =============
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'SET_LOCALE':
return { ...state, locale: action.payload };
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
default:
return state;
}
}
// ============= 初始状态 =============
const initialState: AppState = {
user: null,
theme: 'light',
locale: 'zh-CN',
isLoading: false,
};
// ============= Provider 组件 =============
interface AppProviderProps {
children: ReactNode;
}
export function AppProvider({ children }: AppProviderProps) {
const [state, dispatch] = useReducer(appReducer, initialState);
// ✅ 使用 useMemo 优化,避免不必要的重新渲染
const value = useMemo(() => ({ state, dispatch }), [state]);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
// ============= 自定义 Hooks =============
export function useAppState() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppState must be used within AppProvider');
}
return context.state;
}
export function useDispatch() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useDispatch must be used within AppProvider');
}
return context.dispatch;
}
// ✅ 细粒度的自定义 Hooks,只订阅需要的状态
export function useUser() {
const state = useAppState();
return state.user;
}
export function useTheme() {
const state = useAppState();
const dispatch = useDispatch();
return {
theme: state.theme,
setTheme: (theme: 'light' | 'dark') => {
dispatch({ type: 'SET_THEME', payload: theme });
},
};
}
// ============= 业务 Hooks =============
export function useAuth() {
const user = useUser();
const dispatch = useDispatch();
const login = async (email: string, password: string) => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
// 模拟 API 调用
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('登录失败');
}
const userData: User = await response.json();
dispatch({ type: 'SET_USER', payload: userData });
} catch (error) {
console.error('Login error:', error);
throw error;
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
const logout = () => {
dispatch({ type: 'SET_USER', payload: null });
};
return {
user,
isAuthenticated: !!user,
isLoading: useAppState().isLoading,
login,
logout,
};
}
// ============= 使用示例 =============
function LoginForm() {
const { login, isLoading } = useAuth();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
try {
await login(email, password);
alert('登录成功!');
} catch (error) {
alert('登录失败,请检查用户名和密码');
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">登录</h2>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">邮箱</label>
<input
type="email"
name="email"
required
className="w-full px-3 py-2 border rounded"
disabled={isLoading}
/>
</div>
<div className="mb-6">
<label className="block text-sm font-medium mb-2">密码</label>
<input
type="password"
name="password"
required
className="w-full px-3 py-2 border rounded"
disabled={isLoading}
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-500 text-white py-2 rounded disabled:bg-gray-300"
>
{isLoading ? '登录中...' : '登录'}
</button>
</form>
);
}
function UserProfile() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <p>请先登录</p>;
}
return (
<div className="p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-4">用户信息</h2>
<p><strong>姓名:</strong> {user?.name}</p>
<p><strong>邮箱:</strong> {user?.email}</p>
<button
onClick={logout}
className="mt-4 px-4 py-2 bg-red-500 text-white rounded"
>
退出登录
</button>
</div>
);
}
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="px-4 py-2 bg-gray-200 rounded"
>
切换到 {theme === 'light' ? '深色' : '浅色'} 主题
</button>
);
}
// ============= 应用入口 =============
export default function App() {
return (
<AppProvider>
<div className="min-h-screen bg-gray-50 p-8">
<ThemeToggle />
<div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8">
<LoginForm />
<UserProfile />
</div>
</div>
</AppProvider>
);
}关键点:这个示例展示了 Context + useReducer 的完整模式:
- 使用 useReducer 集中管理状态逻辑
- 使用 useMemo 优化 context value,避免不必要的重新渲染
- 创建细粒度的自定义 Hooks(useUser, useTheme),只订阅需要的状态
- 封装业务逻辑(useAuth),提供简洁的 API(login, logout)
避坑指南
❌ 陷阱 1: 每次 render 创建新的 value
如果在 Provider 组件中直接创建对象或数组作为 value,每次父组件渲染时都会创建新的引用, 导致所有消费该 context 的组件不必要地重新渲染。
❌ 错误代码
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// ❌ 每次渲染都创建新对象!
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 即使 theme 没变,父组件重新渲染时,
// { theme, setTheme } 是新对象,
// 所有使用 useContext 的组件都会重新渲染✅ 修正代码
import { useMemo } from 'react';
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// ✅ 使用 useMemo 缓存 value
const value = useMemo(
() => ({ theme, setTheme }),
[theme] // 只有 theme 变化时才创建新对象
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}心智模型:React 使用 Object.is 比较新旧 context value。 如果每次渲染都是新对象,React 会认为值变了,触发所有订阅组件重新渲染。 使用 useMemo 缓存对象,只在依赖变化时才创建新对象。
❌ 陷阱 2: 所有状态放一个 Context
将所有应用状态放在一个 context 中会导致性能问题:任何一个状态变化都会导致所有消费该 context 的组件重新渲染,即使它们只使用了其中一部分状态。
❌ 错误代码
// ❌ 所有状态放在一个 context
interface AppState {
user: User | null;
theme: 'light' | 'dark';
locale: string;
notifications: Notification[];
cart: CartItem[];
// ... 更多状态
}
const AppContext = createContext<AppState | null>(null);
function AppProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={state}>
{children}
</AppContext.Provider>
);
}
// 问题:当 cart 变化时,
// 只使用 theme 的组件也会重新渲染✅ 修正代码
// ✅ 按变化频率拆分 context
const UserContext = createContext<UserContextType | null>(null);
const ThemeContext = createContext<ThemeContextType | null>(null);
const CartContext = createContext<CartContextType | null>(null);
function AppProvider({ children }) {
return (
<UserProvider>
<ThemeProvider>
<CartProvider>
{children}
</CartProvider>
</ThemeProvider>
</UserProvider>
);
}
// 优势:cart 变化不会影响使用 theme 的组件❌ 陷阱 3: 组件在 Provider 外部使用 useContext
如果组件不在对应的 Provider 内部,useContext 会返回默认值(通常是 null 或 undefined), 导致运行时错误或难以调试的问题。
❌ 错误代码
function Button() {
const { theme } = useContext(ThemeContext);
// 如果不在 Provider 内,theme 可能是 undefined
return <button className={theme}>点击</button>;
}
// ❌ 直接使用,可能出错
function App() {
return <Button />; // 没有包裹 Provider!
}✅ 修正代码
// ✅ 自定义 Hook 中检查
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error(
'useTheme must be used within ThemeProvider'
);
}
return context;
}
function Button() {
const { theme } = useTheme(); // 安全
return <button className={theme}>点击</button>;
}
// ✅ 确保包裹 Provider
function App() {
return (
<ThemeProvider>
<Button />
</ThemeProvider>
);
}❌ 陷阱 4: 在条件语句或循环中使用 useContext
useContext 是 Hook,必须遵循 Hooks 规则:只能在组件顶层调用,不能在条件语句、循环或嵌套函数中调用。
❌ 错误代码
function Component({ showTheme }) {
if (showTheme) {
// ❌ 在条件语句中调用 Hook!
const theme = useContext(ThemeContext);
return <div>{theme}</div>;
}
return <div>No theme</div>;
}
function Component() {
const items = [1, 2, 3];
return items.map(item => {
// ❌ 在循环(回调函数)中调用 Hook!
const theme = useContext(ThemeContext);
return <div key={item}>{theme}</div>;
});
}✅ 修正代码
// ✅ 始终在组件顶层调用
function Component({ showTheme }) {
const theme = useContext(ThemeContext);
if (showTheme) {
return <div>{theme}</div>;
}
return <div>No theme</div>;
}
function Component() {
const theme = useContext(ThemeContext);
const items = [1, 2, 3];
return items.map(item => (
<div key={item}>{theme}</div>
));
}最佳实践
✅ 1. 按变化频率拆分 Context
将频繁变化的状态和不变化或很少变化的状态拆分为不同的 context,避免不必要的重新渲染。
// ✅ 静态配置:很少变化
const ConfigContext = createContext({
apiUrl: 'https://api.example.com',
features: { featureA: true, featureB: false },
});
// ✅ 用户数据:登录后不变
const UserContext = createContext<User | null>(null);
// ✅ 主题:偶尔变化
const ThemeContext = createContext<Theme>('light');
// ✅ 购物车:频繁变化
const CartContext = createContext<CartContextType | null>(null);✅ 2. 分离 State 和 Dispatch
对于使用 useReducer 的场景,将 state 和 dispatch 拆分为两个 context。 这样只读取 dispatch 的组件不会因为 state 变化而重新渲染。
// ❌ 所有组件都在意 state 变化
const AppContext = createContext({ state, dispatch });
// ✅ 拆分为两个 context
const AppStateContext = createContext<AppState | null>(null);
const AppDispatchContext = createContext<Dispatch<Action> | null>(null);
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppStateContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
}
// ✅ 只需要 dispatch 的组件不会因 state 变化而重新渲染
function SaveButton() {
const dispatch = useContext(AppDispatchContext);
const handleClick = () => {
dispatch({ type: 'SAVE' });
};
return <button onClick={handleClick}>保存</button>;
}✅ 3. 创建自定义 Hook 封装
创建自定义 Hook 封装 useContext,提供类型安全和错误检查。
// ✅ 推荐模式:自定义 Hook + 类型安全
function useTheme() {
const context = useContext(ThemeContext);
if (context === null) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// 使用时更简洁,且有类型提示
function Button() {
const { theme, setTheme } = useTheme();
// ...
}✅ 4. 使用 TypeScript 严格类型
避免使用 | null 作为 context 类型,而是创建两个版本的 context: 一个用于 Provider(包含值),一个用于消费者(确保值存在)。
// ✅ 创建不包含 null 的 context 类型
type ThemeContextType = {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
// ✅ 创建类型断言的 Hook
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context; // TypeScript 知道这里不会是 null
}