跳到主要内容

XState

XState 适合处理这样一类问题:状态不只是“存起来”,而是要按明确规则流转。

比如:

  • 一个弹窗有 closed / opening / open / closing
  • 一个表单有 idle / editing / submitting / success / error
  • 一个支付流程会经过“创建订单、等待确认、支付成功、支付失败、重试”
  • 一个多步骤向导有前进、后退、中断、恢复

这类场景如果只用普通 store 去装数据,项目前期往往还能撑住。等分支一多,问题就会出现:

  • 哪些状态能互相跳转,开始变得说不清
  • 某些非法分支只能靠 if 判断临时兜底
  • 异步流程、重试、取消、超时混在一起之后,很难靠脑子维持全局图

这就是 XState 真正有价值的地方。

官方把它定义成一个面向 JavaScript 和 TypeScript 的状态管理与流程编排方案。它结合了事件驱动、状态机、statecharts 和 actor model,不只是“放状态”,而是把流程规则也显式建出来。它既能放在前端,也能跑在后端,React、Vue、Svelte 这些框架都能接。官方文档目前也明确推荐优先看 v5 体系。XState docs

它更适合放在哪

这一篇更适合放在:

docs/state-management/XState.md

不是放在 frameworks/React,也不是放在 foundations/typescript

原因很简单:

  • XState 的核心是状态建模和流程控制
  • 它不是 React 专属库,Vue、Svelte、Node 里也能用
  • 它当然有 TypeScript 体验,但重点不在“类型技巧”,而在“状态图和事件流”

所以它更像状态管理专题里的一条特殊路线:不是全局 store 派,而是显式状态机派。

XState 到底解决什么问题

很多状态管理工具解决的是“值放哪、怎么改、谁来订阅”。

XState 更进一步,它解决的是:

  • 当前系统到底处于哪个状态
  • 哪个事件可以触发跳转
  • 跳转时要做什么动作
  • 哪些异步过程属于这个状态
  • 哪些分支根本不应该发生

换句话说,普通 store 更像“状态容器”,XState 更像“流程说明书 + 运行时”。

一个最小例子

官方文档里最基础的例子就是计数器,不过更能体现它价值的,通常是带状态切换的表单或请求流程。

import { assign, createActor, createMachine } from "xstate";

const formMachine = createMachine({
initial: "idle",
context: {
value: "",
error: "",
},
states: {
idle: {
on: {
EDIT: {
target: "editing",
},
},
},
editing: {
on: {
CHANGE: {
actions: assign({
value: ({ event }) => event.value,
}),
},
SUBMIT: {
target: "submitting",
},
},
},
submitting: {
on: {
SUCCESS: {
target: "success",
},
FAIL: {
target: "error",
actions: assign({
error: ({ event }) => event.message,
}),
},
},
},
success: {},
error: {
on: {
RETRY: {
target: "editing",
},
},
},
},
});

const actor = createActor(formMachine).start();

这段代码的重点不是语法,而是你已经能一眼看出:

  • 状态有哪些
  • 哪些事件会触发跳转
  • 错误只会在 FAIL 里进入 error
  • 成功后不会莫名其妙跳回 submitting

这就是显式状态图的好处。

它最适合哪些场景

不是所有状态都值得上状态机。XState 最适合的是那些“流程复杂度高于数据复杂度”的问题。

1. 多步骤流程

比如:

  • 登录 + 二次验证
  • 支付确认
  • 多步骤表单
  • 引导流程
  • 订单状态流转

这种地方最怕的不是状态值多,而是跳转规则乱。

2. 异步流程很多,而且要处理中断

比如:

  • 请求发起后可取消
  • 失败后允许重试
  • 超时后进入另一分支
  • 某个步骤完成前,后续步骤不能提前发生

如果你已经开始写一堆布尔值,比如:

{
loading: boolean;
success: boolean;
failed: boolean;
canRetry: boolean;
cancelled: boolean;
}

那通常就是个信号:这件事可能更适合状态机,而不是继续堆 flag。

3. 你需要明确限制非法状态

普通对象状态最大的问题,不是“写不出来”,而是太容易写出不该存在的组合。

比如:

  • loading === true 同时 success === true
  • closed === true 但又允许点击“关闭动画完成”
  • 已经失败了,但还停留在“提交中”

状态机的价值就在于把这些不该存在的组合收紧成合法状态集合。

XState 和普通状态库的区别

Redux / Zustand / Jotai 这类库更像什么

它们更像:

  • 一块集中或分散的状态存储
  • 一套更新状态的机制
  • 一种订阅视图更新的方式

它们很适合:

  • 全局用户信息
  • 主题切换
  • UI 开关
  • 轻量业务状态
  • 缓存以外的共享前端状态

XState 更像什么

XState 更像在回答:

  • 系统现在处于哪个节点
  • 事件从哪来
  • 哪个事件在当前节点是合法的
  • 跳转之后要触发什么 side effect

它不是拿来替代一切状态管理的,而是拿来处理“流程本身就是核心复杂度”的那部分逻辑。

ts-pattern 的边界

前一篇 ts-pattern 主要解决的是“复杂分支怎么写得更清楚”。

XState 解决的是“复杂流程怎么建模,怎么限制状态转换”。

两者有交集,但不是一回事:

  • ts-pattern 更像分支表达工具
  • XState 更像流程建模工具

如果你只是有几种联合类型要分发处理,先看 ts-pattern

如果你已经需要画状态图、解释事件流、限制状态跳转,那就该看 XState 了。

React 里常见的用法

官方包里提供了 @xstate/react,可以把 machine 或 actor 接进 React 组件树。但真正的重点不是 hook 名字,而是先把 machine 本身建对。

典型用法通常是:

  • machine 负责描述状态和事件
  • actor 负责运行 machine
  • React 组件只订阅当前状态并触发事件

这样做的好处是,UI 不再自己偷偷维护一套隐形流程。

什么时候值得上 XState

可以先看几个很直接的判断。

适合上

  • 流程有明显阶段
  • 阶段切换规则很多
  • 异步分支、重试、取消、超时都存在
  • 你已经需要画图才能和同事讲清流程
  • 业务里“非法状态”代价很高

不适合上

  • 只是几个简单开关
  • 单页里一两段轻量交互
  • 团队还没准备接受状态机心智
  • 当前复杂度还远没到流程建模这一步

如果只是一个简单 modal 开关,useState 就够了。不要为了“体系完整”给所有小交互都套一层 machine。

一条很实用的经验

如果你现在的代码里已经出现下面这些信号,XState 往往值得认真考虑:

  • 一个流程靠 5 个以上布尔值拼起来
  • 同一个异步流程有开始、成功、失败、取消、重试
  • 新同事很难从代码里看懂“下一步会去哪”
  • 出 bug 时大家争论的不是数据,而是“这个状态到底能不能跳到那里”

这种时候,继续往 store 里补字段,通常只会让问题更隐蔽。

和这个站点当前目录的关系

放到 state-management 这组以后,它的角色会比较清楚:

  • Redux / Zustand / Jotai / Pinia 更偏常规状态存储方案
  • XState 更偏显式流程建模方案

也正因为这样,它很适合和状态管理对比页放在一起看。读者能更快看明白:不是所有状态问题都该用同一种工具。

小结

XState 值得加,而且更适合放在 状态管理 模块下。

它最适合的不是“所有前端状态”,而是那些流程复杂、状态切换必须明确、非法分支需要被硬性限制的场景。

如果你写到后来已经发现:

  • store 里的 flag 越来越多
  • 分支跳转越来越难解释
  • 异步流程的取消、重试、超时混成一团

那就别只把它当成“另一个状态库”。它更像是在给复杂流程补一张可执行的状态图。

参考资料