1、provide
Next.js + React Query + Zustand
-
桥梁 (Bridge):把 Server State (React Query 的数据) 同步到 Client State (Zustand Store)。
-
配置 (Configuration):初始化全局配置(比如注入 API Client 的 getter),确保 React 树以外的工具能正常工作。
-
守卫 (Guard):(可选但常见) 决定“谁能进,谁该滚”(路由保护)。
一个“优雅”的
UserProvider在你的架构(Next.js + React Query + Zustand)中,一个优雅的 Provider 应该具备以下 3 大职责:
-
桥梁 (Bridge):把 Server State (React Query 的数据) 同步到 Client State (Zustand Store)。
-
配置 (Configuration):初始化全局配置(比如注入 API Client 的 getter),确保 React 树以外的工具能正常工作。
-
守卫 (Guard):(可选但常见) 决定“谁能进,谁该滚”(路由保护)。
优雅重构版 UserProvider
UserProvider这是我为你重构的代码。它利用了 Zustand 的脱离 React 组件访问状态的能力,消除了复杂的
useEffectTypeScript
"use client"; import { usePathname, useRouter } from "next/navigation"; import { type ReactNode, useEffect } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { useMe } from "@/hooks/api/user"; import { useAuthStore } from "@/stores/auth-store"; import { setDeptIdGetter } from "@/lib/api-client"; // 假设这是你的 api client 路径 // 定义不需要鉴权的白名单路径 const PUBLIC_PATHS = ["/login", "/signup", "/forgot-password"]; export function UserProvider({ children }: { children: ReactNode }) { const pathname = usePathname(); const router = useRouter(); // 判定当前是否在公共页面 const isPublicPage = PUBLIC_PATHS.includes(pathname); // --- 职责 1: 配置 API Client (最优雅的改动) --- // 利用 Zustand 的 .getState() 方法。 // 不需要依赖 React 的渲染周期,也不需要 ref。 // 只要组件挂载一次,API Client 就能永远获取到 store 里最新的值。 useEffect(() => { setDeptIdGetter(() => { // 🔥 核心魔法:直接从 Store 实例拿值,而不是从 Hook 拿 return useAuthStore.getState().currentDeptId; }); }, []); // --- 职责 2: 数据获取与同步 --- // 只在非白名单页面,或者我们要尝试恢复会话时请求 const { data, error, isLoading } = useMe({ retry: false, // 失败了别重试,直接跳登录 enabled: !isPublicPage // 公开页面不查用户信息,节省资源 }); const setAuth = useAuthStore((s) => s.setAuth); const clearAuth = useAuthStore((s) => s.clearAuth); // 监听数据变化,同步到 Zustand useEffect(() => { if (data) { setAuth(data); } else if (error) { // 只有在非公开页面遇到错误才清理和跳转 // 防止在 Login 页面因为之前的过期 Token 导致循环跳转 clearAuth(); if (!isPublicPage) { router.replace("/login"); // 使用 replace 防止用户点后退又回到受保护页面 } } }, [data, error, isPublicPage, router, setAuth, clearAuth]); // --- 职责 3: 渲染控制 (防闪烁) --- // 1. 如果是公开页面,无条件放行 if (isPublicPage) { return <>{children}</>; } // 2. 如果正在加载用户信息,显示骨架屏(防止未登录用户看到受保护内容一闪而过) if (isLoading) { return <FullPageSkeleton />; } // 3. 如果加载完了但没数据(且有错误),通常 useEffect 会处理跳转, // 但为了安全,这里返回 null 防止渲染子组件 if (!data) return null; // 4. 正常渲染 return <>{children}</>; } // 抽离骨架屏组件,保持代码整洁 function FullPageSkeleton() { return ( <div className="flex h-screen w-full flex-col gap-4 p-8"> <div className="flex items-center gap-4"> <Skeleton className="h-12 w-12 rounded-full" /> <Skeleton className="h-8 w-[200px]" /> </div> <Skeleton className="h-[200px] w-full rounded-xl" /> <div className="space-y-2"> <Skeleton className="h-4 w-full" /> <Skeleton className="h-4 w-[80%]" /> </div> </div> ); }
💡 为什么这个版本更优雅?
1. 解决了 useEffect
闭包地狱 (Line 26-31)
useEffect你的原版:依赖 authStore 钩子的返回值。如果 Store 更新,Effect 可能会重新运行(取决于具体写法),或者陷入闭包陷阱拿到旧值。
优雅版:
TypeScript
setDeptIdGetter(() => useAuthStore.getState().currentDeptId);
这是 Zustand 的杀手锏。
useAuthStore.getState()2. 路由跳转使用 replace
(Line 52)
replace在鉴权失败时,使用
router.replacepush-
场景:用户 Token 过期 -> 被踢到登录页。
-
Push:用户点浏览器“后退” -> 回到受保护页 -> 再次触发错误 -> 又被踢回登录页(死循环,用户体验极差)。
-
Replace:替换当前历史记录,用户点“后退”会回到上一个正常的页面(比如谷歌搜索页)。
3. 职责边界清晰
-
API Client:完全不知道 React 的存在,只知道“有个函数能给我 ID”。
-
React Query:负责与服务器通信,处理 Loading/Error 状态。
-
Zustand:负责作为全局数据的“真理来源 (Single Source of Truth)”。
-
UserProvider:仅仅是把上面三个东西组装起来的胶水层。
4. 白名单机制 (Line 12)
将
isPublicPagePUBLIC_PATHS/forgot-password/termsif总结
优雅的 Provider 不做复杂的业务逻辑,它只做 “同步” 和 “分发”。
通过使用 useAuthStore.getState(),你彻底解耦了 API 层和 UI 渲染层,这是 React + Zustand 架构中最精髓的部分。