关于 xstate-v5
xstate-v5 是面向编码 Agent 的技能,用于设计、编写、审查和迁移 XState v5 状态机及 Actor 系统的 TypeScript 代码。它指导如何在 XState 与 @xstate/store 之间选择,并根据现有代码调整做法。
主要功能
- 01XState v5 状态机与 Actor 设计
- 02选择 XState 或 @xstate/store
- 03TypeScript 编写与代码审查
- 04局部迁移 v4 至 v5
- 05React、Vue、Svelte 和 Solid 集成
适用场景
- 01建模包含状态与转换的工作流
- 02审查或修复 XState 代码库
- 03将旧版 v4 模式迁移到 v5
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ statelyai/ skills --skill xstate-v5安装后在 Claude Code 中输入/xstate-v5确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonextract_and_typecheck.pyregrade_weighted.pyreferences/adapters.mdadvanced-patterns.mdexamples.mdobservables-and-inspection.mdv4-to-v5-quick-ref.md详细介绍
XState v5
优先将此技能用于状态机和状态图工程,其次再考虑 API 正确性。
此技能仅适用于 v5。如果示例、博客文章、解答或本地代码带有 v4 的痕迹,请将其转换为 v5,而不要混用不同版本。优先参考本地仓库代码和官方 v5 文档,而非泛泛的记忆。
你的任务:
- 在
xstate和@xstate/store之间做出选择 - 根据杂乱的需求设计可靠的机器或 Actor 系统
- 以一致的风格编写现代 XState v5 TypeScript 代码
- 审查、修复或改进现有 XState 代码
- 出现旧式 v4 模式时,将其迁移
- 当问题不只是
assign(...)加上fromPromise(...)时,选择合适的 Actor 类型和动作形式 - 在需要时,将机器和 Actor 连接到
@xstate/react、@xstate/vue、@xstate/svelte或@xstate/solid
初步检查
在现有代码库中:
- 检查本地
package.json文件和导入语句。 - 阅读附近的机器、存储、Actor 和适配器用法。
- 在决定采取多强硬的意见之前,先区分新代码、本地编辑和迁移工作。
- 默认保留周边代码的风格。
任务模式
编写代码前先选择一种模式:
- 新代码:优先采用此技能中的现代 v5 模式。
- 本地编辑:保留本地结构和命名,除非现有代码混乱、存在问题,或用户要求进行整理。
- 迁移:优先采用最小且安全的 v5 转换。除非用户要求更广泛的规范化,否则保留结构和语义。
对于迁移和本地编辑,除非为确保正确性所必需、能显著降低本地复杂度,或用户明确要求,否则不要引入 setup(...)、命名实现、对象形式的守卫/动作、标签或 Actor 分解。
选择工具
当领域属于简单的事件驱动状态管理时,优先使用 @xstate/store:
- 没有需要协调、编排或显式建模生命周期的重要有限模式
- 没有调用的异步进程
- 没有 Actor 间通信
- 不需要状态机或状态图概念,例如带守卫的转换、延迟转换、并行状态或历史状态
如果不需要机器级编排,简单的获取或变更逻辑也可以使用 @xstate/store。
当领域具备以下一项或多项特征时,优先使用 XState:
- 会影响行为或 UI 的有限模式
- 异步工作流、重试、取消或后台进程
- 多个相互作用的进程或子 Actor
- 显式守卫、延迟、标签或转换规则
- 需要建模业务流程,而不只是更新状态
如果问题简单到不需要机器,请直言,并推荐使用 @xstate/store。
工作流
需求模糊时:
- 先勾勒机器的结构。
- 为重要的状态、事件、上下文、Actor 和标签命名。
- 简要指出不确定之处或需要权衡的地方。
- 然后编写代码。
需求明确时,简短勾勒结构或省略此步骤,直接编写代码。
不要为了概念上的纯粹而过度重构现有代码。在重要之处改进模型,但若没有充分理由进行大幅重写,就保留可正常工作的结构。
建模问题
在编写或修改代码前,请先思考以下问题:
- 有哪些有限状态?将模式放在状态中,而不是上下文中。
- 上下文中应该包含什么?只保留持久数据,不要存放可推导的布尔值或重复的模式信息。
- 有哪些领域事件?优先使用有意义的名称,通常以点号分隔,例如
form.submitted或order.confirmed。 - 哪些情况应该使用守卫,哪些应该使用独立的分支状态?
- 哪些副作用应该通过调用 actor 来执行,哪些应该作为转换动作?
- 事件能否携带所需数据,而不是先将临时中转数据存入上下文,留待后续状态使用?
fromPromise(...)真的是合适的 actor 吗?还是说这是一个回调/订阅协议,应该随着时间推移将事件发送回来?- 状态机是否应该向外围系统发送事件,而不是让消费者根据上下文推断所有信息?
- 某个子级关注点是否应该作为生成/调用的 actor,而不是让父级变得更加复杂?
- 父级真的需要这些额外的中间状态吗?还是应该由子 actor 负责管理这些内部细节?
- 哪些状态需要使用标签来表示
'loading'、'error'或'dirty'等 UI 语义? - UI 能否使用
snapshot.matches(...)、标签或snapshot.can(...),而不是添加额外的布尔值?
首选的 v5 模式
对于新代码,除非本地代码库有充分理由不这样做,否则优先采用以下模式:
- 仅使用 TypeScript。
- 优先使用
setup({...}).createMachine({...})。 - 在
setup(...)中定义命名的actions、guards、actors和delays。 - 当能提升一致性时,优先使用
{ target: 'next' }这样的转换对象,而不是简写形式。 - 当能保持结构一致时,即使只有一个
action,也优先使用数组。 - 当意图可复用或有名称时,优先使用
{ type: 'track' }或{ type: 'isValid', params: ... }这样的action和guard对象。 - 优先使用
tags: []表示 UI 语义和跨领域的状态含义。 - 优先采用面向领域的事件名称;如果本地命名约定已确立,则沿用该约定。
- 在并行状态中,将转换保留在生命周期需要变化的区域或子状态内。避免使用区域根节点转换去指向同级区域,或使用
reenter: true,除非确实有意重置整个源区域。 - 派生值优先使用普通函数,而不是存储在上下文中。
- 如果某个状态只需将数据传递给下一步,优先使用事件负载,而不是临时通过上下文中转。
- 优先通过
params向命名的actions和guards传递数据,而不是直接访问event。 - 不要将
assign(...)视为唯一的action模式。对于简单上下文更新以外的可复用类型化逻辑,可考虑使用const machineSetup = setup(...)对象中作用域限定于setup的辅助函数,例如machineSetup.createAction(...)、machineSetup.enqueueActions(...)和machineSetup.emit(...);如果命名的action对象更适合该行为,也可以使用它们。 - 当命名的
assign(...)action更新多个上下文属性时,每个属性更新器共享相同的params类型。对所有更新字段使用一个一致的params对象,或将工作拆分为多个独立的命名actions。不要在同一个赋值器中混用不兼容的params形状。 - 当实现必须检查
event且其类型不明显时,优先使用assertEvent(...)进行类型收窄。 - 在示例和最终代码中避免使用
as any和其他宽松类型断言,除非没有更合适的本地方案。 - 有意选择 actor 逻辑:单个请求对应单个结果时优先使用
fromPromise(...);订阅、计时器、外部回调以及会随时间发送事件的多事件协议则优先使用fromCallback(...)。 - 当状态机需要直接通知外围系统时,可考虑使用
emit(...)。 - 尽可能让父状态保持粗粒度。如果额外的中间状态主要是在建模实现细节,优先让子 actor 管理这些内部行为。
- 如果展示多个文件,请确保它们完整接通。为引用的模块和组件提供实际的导入/导出,或者缩小示例范围。
保持事件命名一致。事件名称中的 . 很有用,也有明确含义,包括用于部分事件描述符和更清晰的领域分组。请参阅关于 事件和转换 以及 assertEvent(...) 的 TypeScript 类型收窄 的官方文档。
当任务涉及超出 assign(...) 的类型化 actions、回调 actor、发出的事件或持久化/恢复时,请参阅 references/examples.md 中的规范代码结构和 references/advanced-patterns.md;关于 fromObservable(...)、检查和浏览器检查器模式,请参阅 references/observables-and-inspection.md。
UI 集成
优先让机器驱动 UI 行为:
- 使用
snapshot.matches(...)检查有限模式 - 使用标签进行语义检查,例如加载、保存或脏状态
- 使用
snapshot.can(...)判断当前事件是否有效 - 避免堆叠多个手动布尔值,重复表达机器中的真实状态
如果组件拥有一个小型本地 actor,通常使用 useMachine(...) 就足够了。
如果 actor 是共享的、生命周期较长或对性能敏感:
- 创建或获取一次 actor ref
- 使用
useSelector(...)读取部分状态 - actor 在子树中共享时,使用 actor context/provider 辅助工具
简明适配器指南请参阅 references/adapters.md。
审查与修复指南
审查或修复 XState 代码时,请留意:
- 将有限模式存储在 context 中,而不是状态中
- 本应拆分为子 actor 的大型机器
- 隐藏在随机回调中或混入 assigner 的副作用
- 在同一机器中混用 v4 和 v5 的概念
- 描述 UI 操作机制而非领域含义的状态名称和事件名称
- 应由
matches(...)、标签或选择器替代的多余布尔值 - 简单问题中错过了使用
@xstate/store的机会 - 命名的
assign(...)动作,其属性更新器隐式期望不同形状的params - 并行区域的根级转换指向同级区域或使用
reenter: true;在 v5 中,这可能重新进入源区域,将其重置为初始子状态,并取消正在执行的 invoke
避免过度拆分。Actor 边界只有在能提升清晰度、归属关系或并发性时才有用。不要为了拆分而拆分。
迁移
存在旧代码时,除非用户要求进行更大范围的迁移,否则应逐步、局部地将代码迁移到 v5。
常见迁移目标包括:
cond->guardschema->typesservices->actorsinterpret(...)->createActor(...)- 旧函数签名 -> 解构后的 v5 参数,例如
({ context, event })
对于迁移任务:
- 优先保留局部结构
- 只更改正确性、兼容性或清晰度所必需的内容
- 除非用户要求,否则不要统一改成完整的首选代码风格
- 说明你选择进行的任何非局部重构
- 不要仅因附近代码中存在无效的 hook 选项写法就予以保留
- 迁移 React 用法时,应优先使用当前有效的 hook 接口,而不是试图将旧式
interpret实现覆盖选项塞进useMachine(...) - 将 v4 代码形态迁移到 v5 前,检查
type: 'parallel'的区域根级转换;将处理器移到所属子状态/区域,或仅在确实需要更广泛地重新进入时,才由父级协调
快速迁移模式请参阅 references/v4-to-v5-quick-ref.md。
持久化
涉及持久化或水合时:
- 持久化快照,而不只是 context,除非仅恢复 context 确实足够
- 保存机器状态时优先使用
actor.getPersistedSnapshot() - 尽可能使用
createActor(machine, { snapshot })或适配器/provider 的快照选项进行水合 - 不要通过猜测状态值并拼凑部分 context 数据来手动恢复
具体的持久化和水合模式请参阅 references/advanced-patterns.md。
测试
除非用户询问测试或要求编写测试,否则不要引入测试指南。
如果用户提出此类需求,请简要说明:
- 测试关键转换和守卫
- 测试 actor 行为的成功路径和失败路径
- 根据机器状态测试 UI,而不是重复实现有状态逻辑
- 适时提及基于模型的测试工具
如果需要链接,可指向 XState 的测试和图/路径生成文档。
可选工具
Stately Studio 和检查工具有助于设计、调试和沟通,但它们是可选的。当用户正在设计机器、想要可视化效果或需要更清晰的调试信息时,可以提及这些工具。
- Stately Studio,用于可视化建模和协作
- Inspection API,用于观察 actor 系统
- Stately Inspector,用于对运行中的应用进行可视化检查
输出格式
优先采用以下输出形式:
- 需求不明确时,先简要勾勒机器。
- 代码。
- 简要说明状态、事件、上下文、actor 和标签。
- 仅在相关时提供迁移说明。
优先提供较少但完整的文件,而不是较多但只部分接通的示例。不要勾勒额外的 shell 文件,除非它们已完全接通。
请参阅 references/examples.md,获取更规范、且可随时间扩展的示例。
最终自检
回答前,快速检查代码是否具备编译条件:
- 来自其他文件的每个 JSX 组件都有实际导入
- 每个导入的符号都由所示文件实际导出
- 每个
send(...)和snapshot.can(...)调用都使用有效的事件对象 - 异步工作使用
actors/fromPromise和invoke.input,而不是旧版结构 - 如果引用提示词或周边系统中的外部辅助函数,请声明其签名,或提供足够的类型定义,让示例能够独立成立
- 迁移示例不会为了保留无用的本地代码,而将无效的实现覆盖对象传入钩子
- 旨在可运行的代码围栏中没有占位 shell 文件、遗漏的导入、
...或伪代码
如果完整的多文件答案变得过于冗长,请移除 shell 文件,保留仍能正确展示该模式的较小文件集。