Skip to content
第 13 章 前端 ⏱ 12 分钟阅读

第 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 ​

维度ContextZustand
学习成本零极低
性能中(全量通知)高(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;复杂状态用状态库

动手练习 ​

  1. 主题切换:做 light/dark 切换,Context + useState + useMemo
  2. 用户登录:模拟登录/登出,跨多层组件显示用户名
  3. 多语言:做中英切换,封装 t 函数
  4. 找坑:以下代码性能问题在哪?
    tsx
    <UserContext.Provider value={{ user, setUser }}>

下一章:第 14 章:useReducer →

本站基于 VitePress 构建 · 由 StackHub 团队维护