关于 react-advanced
为使用 TanStack 生态和 XState 构建现代 React 应用提供跨平台约定与架构指导,面向 React Web 和 React Native 开发者。内容规定数据、界面状态、表单与异步流程的职责划分,并说明常见陷阱。
主要功能
- 01区分服务端与客户端状态
- 02规定各类库的职责边界
- 03提供 XState 使用判断依据
- 04说明组件组合模式
- 05按主题提供参考文档
适用场景
- 01规划 React 应用的状态管理
- 02设计多步骤异步交互流程
- 03避免在 useEffect 中处理派生状态
- 04确定表单与服务端数据的管理方式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trancong12102/ agentskills --skill react-advanced安装后在 Claude Code 中输入/react-advanced确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/form.mdintegration.mdreact-query.mdtable.mdtesting.mdxstate.mdzod.mdzustand.md详细介绍
React 高级:核心模式与约定(跨平台)
本技能定义了使用 TanStack 生态系统和 XState 构建现代 React 应用的规则、约定和架构决策。它有意采用明确的主张, 以避免常见陷阱并推行可扩展的模式。
这些模式在 Web 和 React Native 上完全相同。平台特定模式请参阅:
- **Web:**参见
react-web-advanced(TanStack Router、Start、Virtual) - **React Native:**参见
react-native-advanced(Expo Router、FlashList、MMKV)
如需了解此处提及的任何库的详细 API 文档,请使用其他合适的工具 (文档查询、网页搜索等)——本技能侧重于如何以及为何使用这些工具, 而非完整的 API 内容。
禁用 useEffect
请勿将 useEffect 用于:
- 获取数据——使用 React Query(
useSuspenseQuery/useQuery)或路由加载器 - 派生状态——在渲染期间计算,或使用
useMemo - 将状态与 props 同步——直接使用 prop,或通过 React
key重置 - 响应用户事件——将逻辑放在事件处理程序中
- 订阅外部存储——使用
useSyncExternalStore - 复杂的异步流程——使用带有
invoke/fromPromise的 XState 状态机
useEffect 的适用场景
合理的情况:
- 与非 React 外部系统同步——DOM API、第三方组件 (地图、图表)、需要挂载/卸载生命周期的命令式库
- 订阅浏览器/原生 API 并进行清理——对于一次性场景,如果
useSyncExternalStore过于 底层(WebSocket 连接、尺寸变化观察器) - 挂载时进行分析/日志记录——不更新状态的即发即弃副作用
- 将 React Query 数据桥接到 XState——通过事件将服务端状态推送到状态机的
useEffect桥接模式(参见references/xstate.md)
使用 useMountEffect 执行仅在挂载时运行的副作用
当你有合理的、仅在挂载时运行的副作用(上面的第 1–3 种情况)时,请使用 useMountEffect
辅助函数,而不是直接使用 useEffect(fn, []):
// utils/useMountEffect.ts
import { useEffect, type EffectCallback } from "react";
// eslint-disable-next-line react-hooks/exhaustive-deps
const useMountEffect = (effect: EffectCallback) => useEffect(effect, []);
export default useMountEffect;
用法:
useMountEffect(() => {
const plugin = $.myPlugin(ref.current);
return () => {
plugin.destroy();
};
});
原因:直接使用 useEffect(fn, []) 会触发 react-hooks/exhaustive-deps lint 规则,
要求开发者证明空数组是有意为之。useMountEffect 会在代码中明确表达“挂载时运行一次”的意图,
并正确消除警告。
替代方案
| 用途(替代 useEffect) | 使用 |
|---|---|
| 获取数据 | useSuspenseQuery + 路由加载器预取 |
| 使用 Promise | use() hook(React 19+)+ <Suspense> |
| 派生/计算值 | 直接计算或 useMemo |
| 订阅外部存储 | useSyncExternalStore |
| 延迟开销较大的渲染 | useDeferredValue / useTransition |
| 编排复杂异步流程 | XState invoke 配合 fromPromise |
| prop 变化时重置状态 | 组件上的 React key prop |
| 用户触发的副作用 | 直接使用事件处理程序 |
状态管理理念
服务端状态与客户端状态——切勿混为一谈
服务端状态(来自 API/数据库的数据)和 客户端状态(仅存在于客户端中的 UI 状态)是本质上不同的问题。将两者混在一起会导致数据过时、重复和难以处理的同步问题。
| 关注点 | 归属方 | 示例 |
|---|---|---|
| 服务端数据 | React Query | 用户、帖子、产品、订单 |
| URL/路由状态 | Router | 路径参数、搜索参数(TanStack Router 或 Expo Router) |
| 复杂 UI 流程 | XState | 多步骤向导、身份验证流程、拖放 |
| 共享客户端 UI | Zustand | 主题、侧边栏、选中项、全局筛选器、偏好设置 |
| 表单字段 | TanStack Form | 输入值、验证错误、提交 |
| Schema 验证 | Zod | 搜索参数、表单验证器、API 契约 |
| 简单的本地 UI | useState | 切换、手风琴展开、输入焦点 |
决策流程图
Is the data from a server / API?
YES -> React Query (queryOptions + useSuspenseQuery)
NO -> Is it in the URL / route params?
YES -> Router (platform-specific: TanStack Router or Expo Router)
NO -> Is it a complex multi-state flow (3+ states, async, guards)?
YES -> XState (useMachine / createActorContext)
NO -> Is it a form field?
YES -> TanStack Form (with Zod validators)
NO -> Is it shared across components / trees?
YES -> Zustand (create store + selectors)
NO -> useState / useReducer
何时选择 XState,而不是 useState/useReducer
在以下情况下使用 XState:
- 存在 3 个或更多互斥状态,且有明确定义的转换
- 异步副作用必须在状态变化时取消(竞态条件)
- 逻辑包含 守卫条件(用于限制状态转换的条件)
- 需要为独立关注点设置 并行状态
- 需要脱离 React 单独测试流程
- 多个步骤需要支持 前进/后退导航(向导)
不要将 XState 用于简单的切换、单个布尔标志或计数器状态。这些情况应使用
useState。
架构:各库的职责
| 层 | 库 | 职责 |
|---|---|---|
| 服务端状态 | React Query | 获取、缓存、失效、后台重新获取 |
| 复杂 UI 状态 | XState | 状态机、Actor 模型、流程编排 |
| 共享客户端 UI | Zustand | 跨组件 UI 状态、偏好设置、选择项 |
| 表单生命周期 | TanStack Form | 字段值、验证、提交 |
| Schema 验证 | Zod | 搜索参数、表单验证器、API 契约 |
| 数据展示 | TanStack Table | 无头排序、筛选、分页、分组 |
| 测试 | Vitest + TL | 单元、组件、集成、状态机测试 |
| 简单的本地状态 | useState | 切换、本地输入、组件作用域内的值 |
黄金法则:将 queryOptions 作为唯一事实来源
只定义一次查询选项,然后在各处导入——加载器、组件、失效处理:
export const postsQueryOptions = queryOptions({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 30_000,
});
export const postQueryOptions = (postId: string) =>
queryOptions({
queryKey: ["posts", postId],
queryFn: () => fetchPost(postId),
staleTime: 30_000,
});
组件组合
复合组件
当一组组件共享隐式状态时,使用基于 Context 的复合组件。由父组件管理状态;子组件通过 Context 使用该状态。对 Context 值进行记忆化处理。
插槽模式
使用命名属性进行类似插槽的组合(header、footer、actions)。避免深度嵌套的渲染属性树。
控制反转
如果为了处理调用方特定的行为而添加布尔属性或分支逻辑,应通过回调、reducer 或渲染函数将该逻辑交还给调用方。出现三个相似的 if 语句,就说明应该反转控制。
常见陷阱
-
在 useEffect 中派生状态 — 在 effect 中计算值并存入 useState 会导致重复渲染。应在渲染期间计算,或使用
useMemo。 -
在客户端状态中存储服务端数据 — 将 API 响应放入 Zustand/Redux,意味着 你需要自行负责缓存和失效处理。请改用 React Query。
-
在 useState 中重复存储 URL 状态 — 直接使用路由器的 search/param 钩子。
-
对同一数据同时使用 React Query 和 XState — React Query 负责获取和缓存。 XState 通过事件接收数据,仅负责编排。
-
在 XState 状态机中调用 React 钩子 — 钩子只能在 React 组件中使用。 在状态机中使用
fromPromise,通过useEffect事件桥接数据。 -
将数组索引用作键 — 使用稳定的 ID(
item.id)。索引键会导致重排、插入或删除时 状态关联错误。 -
在组件内部定义组件 — 每次渲染都会创建新的组件类型, 导致 React 卸载并重新挂载组件。请在模块级别定义组件。
-
对高频状态使用 Context — 每次状态变化都会导致 Context 的所有使用者重新渲染。 对共享且快速变化的值,使用带选择器的 Zustand(参见
references/zustand.md);如果状态仅限于组件,则使用本地状态。 -
Zod 搜索参数架构未使用
.catch()—.default()只处理缺失的键;.catch()还会处理格式错误的 URL 中的无效值。
参考文件
使用特定库时,请阅读相关参考文件:
| 文件 | 阅读时机 |
|---|---|
references/react-query.md | 查询模式、变更、缓存、Suspense 集成 |
references/table.md | 列定义、排序、筛选、分页、服务端操作 |
references/form.md | 字段验证、数组、架构验证、性能 |
references/xstate.md | 状态机、Actor、身份验证流程、向导、React 集成 |
references/zustand.md | 共享客户端 UI 状态、选择器、切片、中间件、原生存储 |
references/zod.md | 架构验证、v4 API、Form 集成、错误处理 |
references/testing.md | Vitest 配置、Testing Library、MSW、测试 Query/Form/XState |
references/integration.md | 组合使用库:Zustand+XState、Query+XState 桥接 |