或朝夕万年

使用 createContext 创建组件能够提供与读取的 上下文(context)

2025-06-25

在 Next.js 中,你可以利用 React 的 createContext API 来创建和管理全局状态,并在组件树中共享数据,而无需手动通过 props 一层层传递。这对于主题、用户认证信息或任何需要在多个组件中访问的数据都非常有用。

1.创建上下文 (Context)

首先,你需要使用 createContext 创建一个上下文对象。通常,我们会将上下文定义在一个单独的文件中,以便于管理。

// /components/features/context/index.tsx
"use client"; // 👈 必须标记为客户端组件,因为这里使用了 React Hooks 和浏览器 API (localStorage)
 
import React, {
  createContext,
  useState,
  useContext,
  useCallback,
  useEffect, // 引入 useEffect Hook 用于副作用操作 (如保存数据到 localStorage)
  PropsWithChildren, // 引入 PropsWithChildren 用于定义 children 属性的类型
} from "react";
 
// --- 类型定义 ---
// 1. 定义通知 (Notification) 对象的接口
interface Notification {
  id: number; // 通知ID,通常为数字
  message: string; // 通知内容,为字符串
}
 
// 2. 定义全局状态 Context 中包含的状态和函数的接口
interface GlobalStateContextType {
  theme: "light" | "dark"; // 主题,只能是 'light' 或 'dark'
  toggleTheme: () => void; // 切换主题的函数,无参数无返回值
  notifications: Notification[]; // 通知列表,是一个 Notification 类型的数组
  addNotification: (message: string) => void; // 添加通知的函数,接受一个字符串消息
  removeNotification: (id: number) => void; // 移除通知的函数,接受一个数字ID
}
 
// --- Context 创建 ---
// 3. 创建全局状态 Context 对象
// 这里明确指定 Context 的类型为 GlobalStateContextType 或 undefined。
// 默认值设置为 undefined,以便在使用 useContext 时可以检查是否在 Provider 内部。
export const GlobalStateContext = createContext<
  GlobalStateContextType | undefined
>(undefined);
 
// --- Provider 组件 ---
// 4. 创建全局状态 Provider 组件
// 使用 PropsWithChildren 来简化 children 属性的类型定义
export const GlobalStateProvider = ({ children }: PropsWithChildren) => {
  // 初始化主题状态:尝试从 localStorage 读取,如果不存在或在服务器端,则默认为 'light'
  const [theme, setTheme] = useState<"light" | "dark">(() => {
    // 检查当前环境是否是浏览器(客户端)
    if (typeof window !== "undefined") {
      const storedTheme = localStorage.getItem("appTheme"); // 从 localStorage 获取保存的主题
      // 严格检查获取到的值,确保它是 'dark' 或 'light',否则使用 'light'
      return storedTheme === "dark" ? "dark" : "light";
    }
    return "light"; // 如果是服务器端渲染或初次加载时,默认使用 'light'
  });
 
  // 初始化通知列表状态:尝试从 localStorage 读取,如果不存在或解析失败,则默认为空数组
  const [notifications, setNotifications] = useState<Notification[]>(() => {
    if (typeof window !== "undefined") {
      const storedNotifications = localStorage.getItem("appNotifications"); // 从 localStorage 获取保存的通知
      try {
        // 尝试将 JSON 字符串解析回数组,如果失败则返回空数组
        return storedNotifications ? JSON.parse(storedNotifications) : [];
      } catch (e) {
        console.error("解析保存的通知失败:", e); // 打印解析错误
        return []; // 解析失败时返回空数组
      }
    }
    return []; // 如果是服务器端渲染,默认返回空数组
  });
 
  // 使用 useEffect 监听 'theme' 状态变化,并将其保存到 localStorage
  useEffect(() => {
    if (typeof window !== "undefined") {
      localStorage.setItem("appTheme", theme); // 将当前主题保存到 localStorage
    }
  }, [theme]); // 依赖数组:当 'theme' 发生变化时,此 effect 会重新运行
 
  // 使用 useEffect 监听 'notifications' 状态变化,并将其转换为 JSON 字符串保存到 localStorage
  useEffect(() => {
    if (typeof window !== "undefined") {
      localStorage.setItem("appNotifications", JSON.stringify(notifications)); // 将通知数组转换为 JSON 字符串并保存
    }
  }, [notifications]); // 依赖数组:当 'notifications' 发生变化时,此 effect 会重新运行
 
  // --- Actions (操作函数) ---
  // 使用 useCallback 优化 toggleTheme 函数,避免不必要的重新创建
  const toggleTheme = useCallback(() => {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  }, []); // 依赖数组为空,表示此函数不会因外部变量变化而改变
 
  // 使用 useCallback 优化 addNotification 函数
  const addNotification = useCallback((message: string) => {
    setNotifications((prevNotifications) => [
      ...prevNotifications, // 展开之前的通知
      { id: Date.now(), message }, // 添加新的通知,id 使用当前时间戳
    ]);
  }, []);
 
  // 使用 useCallback 优化 removeNotification 函数
  const removeNotification = useCallback((id: number) => {
    setNotifications(
      (prevNotifications) =>
        prevNotifications.filter((notif) => notif.id !== id) // 过滤掉指定 id 的通知
    );
  }, []);
 
  // 将所有状态和操作函数打包成一个对象,作为 Context 的 value
  const contextValue: GlobalStateContextType = {
    theme,
    toggleTheme,
    notifications,
    addNotification,
    removeNotification,
  };
 
  return (
    // 提供 Context 的值给所有子组件
    <GlobalStateContext.Provider value={contextValue}>
      {children}
    </GlobalStateContext.Provider>
  );
};
 
// --- Hook for Context Consumption ---
// 5. 创建一个自定义 Hook,方便在组件中使用全局状态(使用此 Hook 的地方,必须声明在客户端 -> "use client")
export const useGlobalState = () => {
  const context = useContext(GlobalStateContext); // 获取 Context 的值
  // 如果 context 为 undefined,说明 useGlobalState 在 Provider 外部被调用,抛出错误
  if (context === undefined) {
    throw new Error("useGlobalState 必须在 GlobalStateProvider 内部使用");
  }
  return context; // 返回 Context 的值
};

2.提供上下文 (Context Provider)

创建上下文后,你需要使用上下文对象的 .Provider 组件来“提供”数据。Provider 组件接收一个 value prop,这个 value 将是所有消费这个上下文的子组件能够访问到的数据。通常,你会在应用的高层级(例如 _app.js 或一个布局组件)包装 Provider。

在 _app.js 中提供上下文 (适用于全局数据)
如果你希望上下文数据在整个 Next.js 应用中都可用,那么在 pages/layout.tsx 中包裹 Provider 是一个常见的做法。

// pages/layout.tsx
import "../styles/tailwindcss.css";
import Header from "@/components/layout/header";
import { GlobalStateProvider } from "@/components/features/context";
 
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body suppressHydrationWarning className="max-w-3xl m-auto p-5">
        <GlobalStateProvider>
          <Header />
          {children}
        </GlobalStateProvider>
      </body>
    </html>
  );
}

3.使用 Hook 读取上下文 (Consuming Context)

这是在函数组件中读取上下文最推荐的方式,它简洁且易于使用。

'use client';
 
import { useGlobalState } from '@/components/features/context';
 
export default function Home() {
  const { theme, toggleTheme } = useGlobalState();
  console.log(theme);
  return (
    <main>
      <button onClick={() => toggleTheme()}>主题切换</button>
    </main>
  );
}