跳到主要内容

前端状态分层规范

版本:1.0 | 更新日期:2026-10-07

技术栈:React 19 · TanStack Query v5 · jotai · Tailwind CSS 4


一、核心原则​

前端状态按数据来源与共享范围分为三层,每层有且仅有一个正确工具:

层级工具管什么不管什么
服务端状态TanStack Query(useQuery / useMutation)来自后端 API 的数据:列表、详情、搜索结果、分析结果纯 UI 交互(展开/折叠、选中行)
全局客户端状态jotai atoms跨页面/跨组件共享的纯 UI 状态:主题、侧边栏折叠、通知、认证任何可以从 API 获取的数据
组件本地状态useState / useReducer单个组件内的 UI 状态:表单输入、弹窗开关、临时选中需要跨组件共享的状态、API 数据

判断标准(按顺序问):

这个数据是从后端 API 来的吗?
├─ 是 → TanStack Query(useQuery 读 / useMutation 写)
└─ 否 ↓
需要跨多个不相关组件共享吗?
├─ 是 → jotai atom
└─ 否 → useState / useReducer

二、TanStack Query 约定​

2.1 何时使用​

  • 所有 GET 请求的数据读取 → useQuery
  • 所有 POST / PUT / DELETE 等写操作 → useMutation
  • 数据有「新鲜度」概念(服务端可能被其他人修改)→ useQuery
  • 需要缓存、去重、自动重试、后台刷新 → useQuery

2.2 Query Key 规范​

所有 query key 统一在 src/lib/queryKeys.ts 的 qk 工厂对象中注册,禁止在组件内手写 key 数组。

// ✅ 正确
import { qk } from "@/lib/queryKeys";
useQuery({ queryKey: qk.analysis.detail(stockCode), ... });

// ❌ 错误
useQuery({ queryKey: ["analysis", stockCode], ... });

Key 结构遵循「域 → 资源 → 参数」层级,已有的 57 个命名空间覆盖了 market / stock / analysis / strategies / tasks 等全部 API 域。

2.3 useMutation 数据同步策略​

Mutation 成功后更新缓存有两种方式,按场景选择:

策略方法适用场景注意事项
即时写入queryClient.setQueryData(key, newData)写操作的返回值就是最新数据(如 AI 分析结果)不触发 refetch,无 loading 闪烁
失效重取queryClient.invalidateQueries({ queryKey })写操作后需要重新 GET 才能拿到最新数据(如修改配置)会触发 isLoading,注意 UI 不要闪全页 spinner
// 即时写入 — 分析结果直接入缓存
const mutation = useMutation({
mutationFn: (code: string) => analyzeStock(code),
onSuccess: (result) => {
queryClient.setQueryData(qk.analysis.detail(code), result);
},
});

// 失效重取 — 配置修改后重拉
const mutation = useMutation({
mutationFn: updateConfig,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: qk.settings.config() });
},
});

2.4 错误分层​

useQuery 的错误(query.error)和 useMutation 的错误(mutation.error)语义不同,不要混用同一个 state:

  • 加载错误(query.error):GET 失败,展示重试按钮
  • 操作错误(mutation.error 或自定义 runError):POST 失败,展示操作失败提示

两者同时存在时,操作错误优先(用户刚触发的操作比历史加载状态更重要)。

2.5 交易时段轮询​

盘中实时数据(行情/盘口/涨跌分布)使用 useTradingQuery(封装了 useQuery + 交易时段判断 + 轮询间隔),不要自己写 refetchInterval + isTradingHours 判断。


三、jotai 约定​

3.1 何时使用​

  • 状态与 API 数据无关,纯客户端 UI
  • 需要跨不相关组件共享(不通过 props 传递)
  • 当前项目 13 个 atom 全部符合此条件:主题、侧边栏、认证、通知、配额弹窗

3.2 何时不用​

  • 数据来自后端 API → 用 TanStack Query
  • 只在单个组件内使用 → 用 useState
  • 父子组件共享 → 用 props 或 useState 提升到父组件

3.3 Atom 定义位置​

所有 atom 定义在 src/store/ 目录,按领域分文件:

文件内容
atoms.ts主题、侧边栏、聊天、活跃股票等 UI 状态
authAtoms.ts认证用户、登录状态
notificationAtoms.ts通知列表、未读计数、操作函数
quotaAtoms.tsAI 配额弹窗

新增 atom 须在对应文件中定义,不要在组件文件内定义 atom(会导致模块耦合和循环依赖)。


四、useState 约定​

4.1 正确使用场景​

  • 表单输入值(<input> 的 value)
  • 弹窗/抽屉开关(dialogOpen)
  • 展开/折叠状态
  • 临时选中项(表格当前行)
  • 组件内的派生计算中间值
  • useMutation 的附加状态(如 runError、partial)

4.2 禁止使用场景​

// ❌ 禁止:useState 存 API 数据
const [stocks, setStocks] = useState([]);
useEffect(() => {
fetch("/api/v1/stocks").then(r => r.json()).then(setStocks);
}, []);

// ✅ 正确:useQuery 管理 API 数据
const { data: stocks } = useQuery({
queryKey: qk.stocks.list(),
queryFn: () => request.get("/stocks"),
});

4.3 迁移优先级​

项目中仍有约 34 个文件使用 useState + useEffect + request 模式获取服务端数据。迁移优先级:

  1. 高频页面(Home / SmartPick / FactorPick / HotTrending)— 用户感知最强
  2. 多组件共享同一数据 — 迁移后可享受 Query 缓存去重
  3. 有轮询需求 — 迁移后可用 refetchInterval 替代手写 setInterval
  4. 低频管理页面 — 收益较低,按需迁移

五、反模式清单​

反模式问题正确做法
useState + useEffect 取 API 数据无缓存、无去重、无自动重试、切换组件重新加载useQuery
useQuery 的 data 复制到 useState两份状态不同步、失去 Query 缓存意义直接用 query.data
jotai atom 存 API 数据无缓存策略、无失效机制、手动管理同步useQuery
useMutation 成功后 invalidateQueries 导致全页闪烁用户看到 loading spinner 替代了已有数据setQueryData 即时写入
组件内定义 jotai atom模块耦合、循环依赖风险定义在 src/store/
手写 query key 数组不同组件 key 不一致导致缓存失效统一用 qk 工厂
useTradingQuery 外再写 isTradingHours 判断重复逻辑、可能不一致直接用 useTradingQuery
单个 error state 混合加载错误和操作错误用户无法区分「加载失败」和「操作失败」分开追踪,操作错误优先

六、迁移检查清单​

将 useState + useEffect 模式迁移到 useQuery 时,逐项确认:

  • 在 qk 中有对应的 query key(没有则新增)
  • queryFn 返回的数据类型与 useState 的泛型一致
  • 设置了合理的 staleTime(静态数据 5min+,实时数据 0)
  • 设置了合理的 gcTime(默认 5min,高频页面可延长到 30min)
  • 移除了 useEffect 中的手动 fetch 逻辑
  • 错误处理从 catch 改为 query.error 或 error 派生
  • Loading 状态从自定义 loading 改为 query.isLoading / query.isFetching
  • 如有写操作,使用 useMutation 并在 onSuccess 中同步缓存

七、当前迁移状态​

类别已完成待迁移
自定义 hooks(src/hooks/)11 个 useQuery + 1 个 useMutation—
jotai atoms(src/store/)13 个(全部为客户端状态)—
页面/组件中的服务端 useState少量~34 个文件

相关文档​