职责:
- 在
queryFn内部直接调用setAuth(写副作用)虽然能跑通,但它被认为是一种反模式 (Anti-pattern),因为它违反了“关注点分离”原则。
为什么不建议在 queryFn 里写 setAuth?
-
幂等性问题:
queryFn可能会因为网络重试、窗口聚焦刷新、缓存失效等原因被多次调用。在里面写状态更新会导致不必要的多次setState。 -
职责不清:
queryFn的唯一职责应该是**“获取并返回数据”。同步 Store、处理重定向属于“响应数据后的副作用”**。 -
调试困难:如果你有多个地方用了
useMe,追踪到底是哪次调用触发了 Store 更新会变得非常混乱。
推荐的“工业级”结构化写法
最具有结构性、让人“一眼看懂”的方案是将逻辑拆分为:纯净的数据层、全局副作用监听、以及受保护的渲染层。
要写出“一眼就能看懂”的工业级代码,理解工具的职责边界是关键。你提到的 QueryClient 正是 TanStack Query(原 React Query)的大脑。
一、 什么是 QueryClient?
简单来说,QueryClient 是一个“智能数据管家”。
在没有它之前,你需要手动管理 loading、error、data 以及“什么时候该重新请求”。有了它之后,它负责维护一个全局的内存缓存(Cache)。
-
它的含义:它是连接你的 React 组件与远程数据的“中间层”。它不关心你的 API 是用 Eden RPC 还是 Axios 写的,它只关心数据是否过期(Stale)、是否需要重构。
-
形象比喻:它像是一个带缓存的图书馆。
-
useQuery是借书请求。 -
QueryClient是图书馆管理员。 -
如果你要的书(数据)在馆内且没过时,管理员直接给你(缓存);如果过时了,管理员去进货(API 请求)。
-
二、 核心编程规范与职责 (Best Practices)
在现代全栈开发中,遵循**“单一职责原则” (Single Responsibility Principle)** 是最高准则。我们可以把职责划分为四层:
1. 数据层 (The Fetcher) —— 纯净与隔离
-
职责:只负责与后端打交道,把原始数据拿回来。
-
规范:
queryFn必须是纯函数。不要在里面写router.push,不要在里面写setAuth。 -
理由:API 报错或重试不应该直接导致全局状态错乱。
2. 状态层 (The Store) —— 单一事实来源
-
职责:存储经过加工的、全应用共享的业务状态(如用户信息、当前站点)。
-
规范:Store 只接收数据并保存,不负责“去哪里拿数据”。
-
理由:这样你的组件无论从哪个 Hook 拿数据,看到的都是同一份结果。
3. 拦截层 (The Provider/Middleware) —— 副作用中心
-
职责:监听数据变化,触发后续动作(同步 Store、持久化 LocalStorage、重定向)。
-
规范:所有的“联动”逻辑(例如:拿到了用户 -> 同步到 Store)应该收拢在这里。
三、 推荐的代码结构规范
为了让你项目中的同事“一眼看懂”,建议采用以下职责分配:
📁 文件组织规范:
-
hooks/api/:放所有的useQuery。职责:定义“怎么拿数据”。 -
stores/:放 Zustand。职责:定义“数据怎么存”。 -
providers/:放逻辑守卫。职责:定义“数据回来后干什么”。
💡 代码编写模版(职责分明版):
代码职责:获取数据 (API Hook)
TypeScript
// hooks/api/use-user.ts
export const useMe = () => useQuery({
queryKey: ['me'],
queryFn: () => rpc.api.v1.me.get().then(handleEden),
// 规范:只定义获取逻辑和缓存策略
});
代码职责:处理副作用 (Provider)
TypeScript
// providers/user-provider.tsx
export function UserProvider({ children }) {
const { data, error } = useMe();
const setAuth = useAuthStore(s => s.setAuth);
// 规范:使用 useEffect 统一处理“数据到达”后的动作
useEffect(() => {
if (data) {
setAuth(data); // 同步到全局
console.log("✅ 用户数据已同步至 Store");
}
}, [data, setAuth]);
// 规范:统一处理认证失败
if (error?.status === 401) return <Redirect to="/login" />;
return <>{children}</>;
}
四、 总结:为什么要这么麻烦?
如果你把所有逻辑(API + Store + Router)都塞进 queryFn,代码会变成**“不可预测的黑盒”**:
-
难以测试:你想测试 API 逻辑,却被迫触发了页面跳转。
-
难以重用:另一个组件只想偷偷拿一下数据,结果它一调用,全局 Store 被重置了。
-
调试地狱:当 Store 里的数据错了,你不知道是哪次 API 请求触发的副作用。
遵循规范后的感受:
-
当数据没拿到:去看
hooks/api/。 -
当权限按钮没出来:去看
stores/auth-store.ts。 -
当页面没跳转:去看
UserProvider.tsx。
这就是所谓的“结构性”——每个文件只讲一个故事。
你想让我针对你目前的权限系统,帮你优化一下那个 PermissionGuard(权限守卫组件)的职责划分吗?这样可以保证即使是复杂的权限,代码依然整洁。