第 13 章:useContext
学习目标
- 理解 Context 解决什么问题(prop drilling)
- 掌握 createContext / Provider / useContext 三件套
- 封装全局状态(Context + useState)
- 避开 Context 的 4 个常见坑
一、Prop Drilling 问题
深层组件传数据很痛苦:
tsx
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<Avatar user={user} /> {/* 只想用 user,中间层都被迫传 */}
</Sidebar>
</Layout>
</App>Context 让组件直接拿到祖先的数据,不用逐层透传。
二、Context 三件套
tsx
import { createContext, useContext } from 'react';
// 1. 创建 Context(可设默认值)
const UserContext = createContext<User | null>(null);
// 2. Provider 在祖先提供值
function App() {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}
// 3. 子组件用 useContext 读取
function Avatar() {
const { user } = useContext(UserContext);
if (!user) return <span>未登录</span>;
return <img src={user.avatar} alt={user.name} />;
}💡 可以创建多个 Context,套在不同层级的 Provider 里——按业务拆 Context(user/theme/lang 各一个),谁需要谁
useContext——别全堆一个,否则一改全组件重渲染。tsx// 多个 Context,各自包各自的 Provider const ThemeContext = createContext<'light' | 'dark'>('light') const UserContext = createContext<User | null>(null) function App() { return ( <ThemeContext.Provider value="dark"> <UserContext.Provider value={user}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ) }Provider 嵌套层级决定谁能拿到:
Provider 在哪 谁能拿到 顶层 App 整个应用 路由 Layout 里 该路由下的所有页面 某个组件里 只在该组件及其后代
三、TypeScript 类型
tsx
interface UserCtx {
user: User | null;
setUser: (u: User | null) => void;
}
const UserContext = createContext<UserCtx | null>(null);
// 封装自定义 hook(类型守卫)
function useUser() {
const ctx = useContext(UserContext);
if (!ctx) throw new Error('useUser 必须在 UserProvider 内使用');
return ctx;
}
// 使用
function Comp() {
const { user, setUser } = useUser();
// ✅ 直接用,ctx 必不为 null
}⚠️ 坑 1:不封装自定义 hook,每个用到 context 的组件都得做 null 判断,重复且易漏。
四、Provider 嵌套
Provider 可嵌套,内层会覆盖外层同名 context:
tsx
<ThemeContext.Provider value="light">
<Layout>
<ThemeContext.Provider value="dark"> {/* 这里开始变 dark */}
<Dashboard />
</ThemeContext.Provider>
</Layout>
</ThemeContext.Provider>五、封装全局 Context 模式
tsx
interface ThemeCtx {
theme: 'light' | 'dark';
toggle: () => void;
}
const ThemeContext = createContext<ThemeCtx | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const value = useMemo<ThemeCtx>(() => ({
theme,
toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')),
}), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme 必须在 ThemeProvider 内');
return ctx;
}⚠️ 坑 2:
Provider value不要直接传新对象{theme, toggle},每次渲染引用变,所有消费者都重渲染。用useMemo缓存。💡 Provider value 别传新对象——对象/数组/函数在 JSX 里都是新引用,每次 render 都触发消费者重渲染。用
useMemo(() => value, deps)缓存引用,依赖不变就不重渲染。
六、多个 Context 分文件
tsx
// contexts/ThemeContext.tsx
export const ThemeContext = createContext<...>(null);
export const ThemeProvider = ...;
export const useTheme = ...;
// contexts/AuthContext.tsx
export const AuthContext = createContext<...>(null);
export const AuthProvider = ...;
export const useAuth = ...;
// App.tsx
function App() {
return (
<ThemeProvider>
<AuthProvider>
<Router />
</AuthProvider>
</ThemeProvider>
);
}七、useContext 触发渲染
tsx
function Display() {
const { theme } = useTheme(); // theme 变化会重渲染
return <div className={theme}>...</div>;
}⚠️ 坑 3:任何使用
useContext的组件都会在 context value 变化时重渲染,无关紧要的子组件会被拖累。
性能优化方案:
- 拆 Context(高频和低频分开)
- 用 selector(见 zustand 第 17 章)
- 用
React.memo包无关子组件
八、Context vs Redux/Zustand
| 维度 | Context | Zustand |
|---|---|---|
| 学习成本 | 零 | 极低 |
| 性能 | 中(全量通知) | 高(selector 订阅) |
| 适用 | 主题、用户、locale | 复杂业务状态 |
| 调试 | 一般 | DevTools 支持 |
⚠️ 坑 4:Context 不是状态管理工具,只是数据传递机制。复杂状态用 Zustand/Redux。
九、实战:多语言切换
tsx
type Locale = 'zh' | 'en';
const dict = {
zh: { hello: '你好', bye: '再见' },
en: { hello: 'Hello', bye: 'Bye' },
};
interface I18nCtx { locale: Locale; setLocale: (l: Locale) => void; t: typeof dict.zh; }
const I18nContext = createContext<I18nCtx | null>(null);
function I18nProvider({ children }: { children: ReactNode }) {
const [locale, setLocale] = useState<Locale>('zh');
const value = useMemo(() => ({ locale, setLocale, t: dict[locale] }), [locale]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
function useI18n() {
const ctx = useContext(I18nContext);
if (!ctx) throw new Error('missing I18nProvider');
return ctx;
}
// 使用
function Hello() {
const { t } = useI18n();
return <h1>{t.hello}</h1>;
}十、本章小结
| 要点 | 关键 |
|---|---|
| 解决问题 | Prop drilling(逐层透传) |
| 三件套 | createContext / Provider / useContext |
| 自定义 hook | 包一层做类型守卫,避免 null 判断 |
| value 缓存 | useMemo 防引用变化导致无谓渲染 |
| 性能 | context 变化通知所有消费者 |
| 适用 | 主题/用户/i18n;复杂状态用状态库 |
动手练习
- 主题切换:做 light/dark 切换,Context + useState + useMemo
- 用户登录:模拟登录/登出,跨多层组件显示用户名
- 多语言:做中英切换,封装 t 函数
- 找坑:以下代码性能问题在哪?tsx
<UserContext.Provider value={{ user, setUser }}>
下一章:第 14 章:useReducer →