useContext:依赖注入与性能陷阱
第9章:useContext:依赖注入与性能陷阱
React Context 是依赖注入机制。它让你把值'广播'到组件树的任意深度,但它的广播本质也意味着性能上的隐患。
本章核心问题:Context 的性能特性是什么?为什么所有消费者都会重渲染? 读完本章你将理解:
- Context 是 React 内置的依赖注入机制,适合低频变化的全局配置
- Context 的广播模型导致所有消费者在 value 变化时全量重渲染
- 拆分 Context、分离读写是解决性能问题的核心策略
Level 1 · 你需要知道的(1-3年经验)
Context 是依赖注入
在软件工程中,**依赖注入(Dependency Injection)**是一种模式:不是让组件自己创建它需要的依赖,而是从外部"注入"进来。这样组件不需要知道依赖从哪里来,也不需要了解依赖的创建细节。
React Context 就是一种依赖注入机制。它让你可以把值"广播"到组件树的任意深度,而不需要通过每一层手动传递 prop(俗称"prop drilling")。
// 创建 Context
const ThemeContext = createContext<'light' | 'dark'>('light');
const UserContext = createContext<User | null>(null);
// Provider:向子树注入值
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [user, setUser] = useState<User | null>(null);
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// 任意深度的消费者
function Avatar() {
const theme = useContext(ThemeContext);
const user = useContext(UserContext);
return (
<img
className={`avatar avatar-${theme}`}
src={user?.avatarUrl}
alt={user?.name}
/>
);
}
Context 特别适合以下类型的数据:
- 主题(theme):整个应用共享的视觉配置
- 当前用户(current user):登录状态、权限
- 语言/国际化(locale):当前语言设置
- 路由状态:当前路径、路由参数
所有消费者都会重渲染:Context 的性能特性
这是 Context 最重要也最容易被忽视的特性:每当 Provider 的 value 发生变化,所有使用 useContext 消费该 Context 的组件都会重渲染,无论这个组件实际上有没有用到变化的部分。
const AppContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
// 每当 notifications 更新,user 没有变,
// 但所有消费 AppContext 的组件都会重渲染
const value = { user, notifications, setUser, setNotifications };
return (
<AppContext.Provider value={value}>
<Header /> {/* 只用了 user,但也会因 notifications 变化重渲染 */}
<NotificationBell /> {/* 只用了 notifications,正确重渲染 */}
<MainContent /> {/* 两者都没用,但也会重渲染 */}
</AppContext.Provider>
);
}
为什么会这样?因为 React 没有办法知道 Header 只关心 user 而不关心 notifications——Context 是整体传递的,消费者要么订阅整个 Context 对象,要么不订阅。
问题的根源:value 对象每次都是新引用
// 错误示范:每次渲染都创建新对象,导致所有消费者每次都重渲染
<AppContext.Provider value={{ user, notifications }}>
即使 user 和 notifications 没有变,{ user, notifications } 这个对象字面量每次渲染都是一个新引用,Context 检测到值变化,所有消费者重渲染。
// 部分缓解:用 useMemo 稳定 value 对象
const value = useMemo(() => ({ user, notifications }), [user, notifications]);
<AppContext.Provider value={value}>
但这只解决了"无意义的重渲染"问题,没有解决"notifications 变化导致只用 user 的组件也重渲染"的问题。
Context vs 其他状态共享方式
Context vs Prop Drilling
Prop drilling 是指通过多层组件传递 prop 的模式。它有一个常被忽视的优点:数据流明确,你可以通过追踪 prop 轻松知道数据从哪里来、传到哪里去。
什么时候 prop drilling 比 Context 更好:
- 组件层级不超过 2-3 层
- 数据只需要传递到少数几个特定组件
- 组件需要高度可复用(Context 会把组件与特定 Context 绑定)
// 2 层传递:prop drilling 完全可以接受,不需要 Context
function App() {
const user = useCurrentUser();
return <Layout user={user} />;
}
function Layout({ user }) {
return <Header user={user} />;
}
function Header({ user }) {
return <span>{user.name}</span>;
}
Context vs 外部状态管理库
外部状态管理库(Zustand、Jotai、Redux Toolkit)与 Context 的根本区别在于性能模型:
- Context:任何 Provider value 变化都重渲染所有消费者
- Zustand/Jotai:支持精确订阅,组件只在它依赖的那一片状态变化时重渲染
// Zustand:精确订阅,只有 user 变化时重渲染
const useStore = create(set => ({
user: null,
notifications: [],
setUser: user => set({ user }),
addNotification: n => set(s => ({ notifications: [...s.notifications, n] })),
}));
function Header() {
// 只订阅 user,notifications 变化不触发重渲染
const user = useStore(state => state.user);
return <nav>{user?.name}</nav>;
}
选择建议:
- Context:适合低频变化的全局配置(theme、locale、auth user)
- 外部状态库:适合高频变化的共享状态、需要精确订阅的场景
- 两者结合:Context 注入状态管理库的 store,是很多大型应用的架构
Level 2 · 它是怎么运行的(3-5年经验)
一个完整的 Context 架构示例
综合本章的最佳实践,构建一个生产级别的认证 Context:
type User = { id: string; name: string; role: 'admin' | 'user' };
type AuthState = { user: User | null; isLoading: boolean };
// 读写分离
const AuthStateContext = createContext<AuthState>({ user: null, isLoading: true });
const AuthActionsContext = createContext<{
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}>({ login: async () => {}, logout: () => {} });
function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({ user: null, isLoading: true });
useEffect(() => {
// 初始化时检查已有 session
checkSession()
.then(user => setState({ user, isLoading: false }))
.catch(() => setState({ user: null, isLoading: false }));
}, []);
// 使用 useMemo 确保 actions 引用稳定(不因 state 变化而重创建)
const actions = useMemo(() => ({
async login(credentials: Credentials) {
const user = await authenticate(credentials);
setState({ user, isLoading: false });
},
logout() {
clearSession();
setState({ user: null, isLoading: false });
},
}), []); // actions 不依赖 state,引用永远稳定
return (
<AuthActionsContext.Provider value={actions}>
<AuthStateContext.Provider value={state}>
{children}
</AuthStateContext.Provider>
</AuthActionsContext.Provider>
);
}
// 封装为自定义 Hook,提供更好的 DX 和类型安全
function useAuthState() {
return useContext(AuthStateContext);
}
function useAuthActions() {
return useContext(AuthActionsContext);
}
// LoginButton 只订阅 actions,不会因为 user 变化而重渲染
function LoginButton() {
const { login } = useAuthActions();
return <button onClick={() => login({ email: '', password: '' })}>登录</button>;
}
// UserGreeting 只订阅 state
function UserGreeting() {
const { user, isLoading } = useAuthState();
if (isLoading) return <Skeleton />;
if (!user) return null;
return <span>你好,{user.name}</span>;
}
Level 3 · 规范怎么定义的(资深)
React 19:use() 消费 Context
React 19 引入的 use() Hook 可以在任何地方消费 Context,包括条件语句内(这是传统 Hooks 做不到的):
import { use } from 'react';
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
// use() 可以在条件语句内使用——这是 useContext 做不到的
if (isLoggedIn) {
const user = use(UserContext);
return <h1>欢迎,{user.name}</h1>;
}
return <h1>请先登录</h1>;
}
use() vs useContext() 的对比:
| 特性 | useContext |
use() |
|---|---|---|
| 可在条件语句中用 | 否 | 是 |
| 可消费 Promise | 否 | 是 |
| 触发 Suspense | 否 | 是(消费 Promise 时) |
| 语义 | 只读 Context | 读 Context 或 Promise |
use() 消费 Context 时行为与 useContext 一致——同样的重渲染语义。它的优势在于灵活性(条件消费)和统一性(同一个 API 消费 Context 和 Promise)。
Level 4 · 边界与陷阱(所有人)
解决 Context 性能问题的策略
策略一:拆分 Context
最简单也最有效的方案:把频繁变化和不常变化的数据分到不同的 Context 里。
// 分离:频率不同的数据用不同 Context
const UserContext = createContext<User | null>(null);
const NotificationsContext = createContext<Notification[]>([]);
const ThemeContext = createContext<Theme>('light');
function App() {
const [user] = useState<User | null>(null);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [theme] = useState<Theme>('light');
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={user}>
<NotificationsContext.Provider value={notifications}>
<AppLayout />
</NotificationsContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// Header 只订阅 UserContext,notifications 变化不影响它
function Header() {
const user = useContext(UserContext);
return <nav>{user?.name}</nav>;
}
策略二:进一步拆分读写 Context
一种常见的 Context 设计模式是把状态和更新函数分开放在两个 Context 里。因为更新函数(如果用 useCallback 或者传递 dispatch)是稳定的引用,不会触发消费者重渲染:
const CountContext = createContext<number>(0);
const CountDispatchContext = createContext<React.Dispatch<number>>(() => {});
function CountProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
return (
<CountDispatchContext.Provider value={setCount}>
<CountContext.Provider value={count}>
{children}
</CountContext.Provider>
</CountDispatchContext.Provider>
);
}
// 这个组件只订阅 dispatch,count 变化不会让它重渲染
function ResetButton() {
const setCount = useContext(CountDispatchContext);
return <button onClick={() => setCount(0)}>重置</button>;
}
// 这个组件只订阅 count
function CountDisplay() {
const count = useContext(CountContext);
return <span>{count}</span>;
}
策略三:Context Selector 模式
有时你确实需要一个大的 Context,但只想在某个特定字段变化时重渲染。React 原生不支持 context selector,但可以用第三方库或者手动实现:
// use-context-selector 库实现 selector
import { createContext, useContextSelector } from 'use-context-selector';
const AppContext = createContext({ user: null, notifications: [] });
// 只有当 user 变化时才重渲染,notifications 变化不影响
function Header() {
const user = useContextSelector(AppContext, ctx => ctx.user);
return <nav>{user?.name}</nav>;
}
手动实现的折中方案:用 React.memo 包裹组件,并把需要的值从 Context 中提取出来作为 prop:
function Header() {
const { user } = useContext(AppContext);
return <HeaderInner user={user} />;
}
// HeaderInner 只有当 user 变化时才重渲染
const HeaderInner = React.memo(function HeaderInner({ user }) {
return <nav>{user?.name}</nav>;
});
这个方案的局限:Header 本身还是会因为任何 Context 变化而重渲染,只是 HeaderInner 的渲染被跳过了——如果 Header 的渲染本身开销不大,这是可以接受的。
什么时候不应该用 Context
Context 常被误用为"全局变量的 React 替代品"。以下场景用 Context 是错误的:
错误一:服务端状态(Server State)
网络请求的数据、缓存、实时更新——这些不适合 Context,应该用专门的数据获取库(React Query、SWR):
// 错误:用 Context 管理服务端状态
const UserDataContext = createContext(null);
function UserDataProvider({ children }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
fetch('/api/me').then(r => r.json()).then(setUserData);
}, []);
// 没有缓存、没有重试、没有失效策略
return <UserDataContext.Provider value={userData}>{children}</UserDataContext.Provider>;
}
// 正确:用 React Query
function useCurrentUser() {
return useQuery({ queryKey: ['me'], queryFn: () => fetch('/api/me').then(r => r.json()) });
}
错误二:表单状态
表单的字段值是局部状态,不需要 Context。即使表单很复杂,也应该用专门的表单库(React Hook Form、Formik)。
错误三:高频更新的状态
如果状态每秒更新多次(鼠标位置、动画进度),Context 不适合——每次更新会重渲染所有消费者。应该用 ref + 命令式更新,或者外部状态库的精确订阅。