- 前端
- 后端
【免费下载链接】xstate
State machines, statecharts, and actors for complex logic
@xstate/react 是 XState 官方提供的 React 集成包,它把状态机(State Machines)、状态图(Statecharts)与 Actor 模型无缝接入 React 组件树。本文以该包的 CHANGELOG.md 为主线,梳理从 v1 到 v6 的关键 API 变更、破坏性迁移点与最佳实践,并对照 packages/xstate-react/src 下的源码实现,帮助你在新老代码库中都能正确、高效地使用useActor、useActorRef、useSelector与createActorContext。
读完本文,你将掌握:如何用 Hooks 绑定状态机并驱动组件渲染、如何在错误发生时借助 React Error Boundary 兜底、如何让组件只订阅 Actor 的局部快照避免多余渲染,以及如何从旧 API 平滑迁移到新的 Actor 模型。
快速上手:一个最小可运行示例
来自 README.md 的 Quick start 给出了最基础的用法。安装依赖:
npm i xstate @xstate/react然后定义一个简单的 toggle 状态机,并通过useMachine绑定到组件:
import { useMachine } from '@xstate/react'; import { createMachine } from 'xstate'; const toggleMachine = createMachine({ id: 'toggle', initial: 'inactive', states: { inactive: { on: { TOGGLE: 'active' } }, active: { on: { TOGGLE: 'inactive' } } } }); export const Toggler = () => { const [state, send] = useMachine(toggleMachine); return ( <button onClick={() => send({ type: 'TOGGLE' })}> {state.value === 'inactive' ? 'Click to activate' : 'Active! Click to deactivate'} </button> ); };useMachine(machine)返回一个元组:当前快照state与send发送事件函数。在 v4 之后,useMachine本质上是useActor的别名,见 useMachine.ts 中的/** @alias useActor */注释与直接转调useActor(machine, options)的实现。
核心 Hooks API 总览
从 src/index.ts 可以看到包当前对外暴露的全部公共 API:
createActorContext—— 创建与 React Context 集成的全局 Actor;useActor—— 绑定任意 Actor 逻辑(机器、Promise、Transition 等),返回[snapshot, send, actorRef];useActorRef—— 返回可直接传给其他组件的ActorRef,不订阅其快照变化;useSelector—— 从 Actor 快照中派生局部状态,并支持自定义比较函数;shallowEqual—— 浅比较辅助函数;useMachine—— 已弃用,仅为useActor的别名,保留用于向后兼容。
useActor 的底层实现
在 useActor.ts 中可以看到它如何把 Actor 接入 React 渲染循环:
- 通过
useIdleActorRef(logic, options)惰性创建 ActorRef(首次渲染时才createActor,见 useActorRef.ts); - 用
useSyncExternalStore订阅 Actor 的快照,subscribe与getSnapshot均用useCallback稳定引用; - 在
useEffect中启动actorRef.start(),卸载时调用stopRootWithRehydration清理。
其中getSnapshot直接取actorRef.getSnapshot(),因此传入的 Actor 逻辑必须具备getSnapshot方法——这正是 v4.0.0 中“移除 hooks 的getSnapshot参数”这一破坏性变更的前提(见下文迁移章节)。
useActorRef 与订阅解耦
useActorRef返回 Actor 引用但不订阅快照,因此适合需要把引用传给子组件、由子组件自行选择订阅方式的场景。它还接受第二个参数observerOrListener,可直接传入Observer对象或回调函数,源码中通过toObserver(observerOrListener)统一转换,并在useEffect中完成订阅与清理(见 useActorRef.ts)。
错误传播:让 Actor 的 error 状态进入 React Error Boundary
CHANGELOG 6.1.0 是本文最值得关注的能力:useActor与useSelector现在会在 Actor 进入 error 状态时主动抛出错误,从而可以被 React Error Boundary 捕获。此前错误只会静默停留在快照的status: 'error'字段里,需要手动处理。
其实现非常直观:useActor在拿到快照后检查'status' in actorSnapshot && snapshot.status === 'error',命中则throw snapshot.error(见 useActor.ts);useSelector的boundGetSnapshot同样在返回前检查并抛出(见 useSelector.ts)。
官方示例——一个可能因网络错误而进入 error 状态的机器:
import { createMachine } from 'xstate'; import { useActor } from '@xstate/react'; import { ErrorBoundary } from 'react-error-boundary'; const machine = createMachine({ initial: 'idle', states: { idle: { on: { fetch: 'loading' } }, loading: { invoke: { src: fromPromise(async () => { throw new Error('Network error'); }), onDone: 'success' // Without onError, the actor enters an error state } }, success: {} } }); function App() { return ( <ErrorBoundary fallback={<p>Something went wrong</p>}> <ActorComponent /> </ErrorBoundary> ); } function ActorComponent() { // If the actor errors, the error will be thrown // and caught by the nearest error boundary const [snapshot, send] = useActor(machine); return <div>{snapshot.value}</div>; }要点:只有进入error 状态(例如invoke的 Promise 抛出且未定义onError)时才会抛错;如果机器通过onError显式处理了错误并转移到普通状态,则不会触发 Error Boundary。这让“全局兜底 + 局部容错”两种策略可以按需混用。
useSelector:局部订阅、自定义比较与可空 Actor
选择器与比较函数
useSelector(actor, selector, compare?)基于useSyncExternalStoreWithSelector实现(见 useSelector.ts),默认使用defaultCompare(严格相等a === b)判断选择结果是否变化,避免无关状态更新引发组件重渲染:
const count = useSelector(someActor, (state) => state.context.count);对于返回新对象的选择器,可以传入自定义比较函数(如包内导出的shallowEqual,实现见 shallowEqual.ts):
const { count, list } = useSelector( actor, (state) => ({ count: state.context.count, list: state.context.list }), shallowEqual );兼容 @xstate/store(4.1.1)
从 4.1.1 起,@xstate/react的useSelector可以直接订阅@xstate/store创建的 store,无需引入@xstate/store/react:
import { createStore } from '@xstate/store'; import { useSelector } from '@xstate/react'; const store = createStore( { count: 0 }, { inc: { count: (context) => context.count + 1 } } ); function Counter() { // Note that this `useSelector` is from `@xstate/react`, // not `@xstate/store/react` const count = useSelector(store, (state) => state.context.count); return ( <div> <button onClick={() => store.send({ type: 'inc' })}>{count}</button> </div> ); }可空 Actor(4.1.0)
当 Actor 可能尚未创建时(例如来自异步逻辑的可选引用),useSelector的actor参数允许为undefined,此时传给选择器的snapshot也可能是undefined:
const count = useSelector(maybeActor, (snapshot) => { // `snapshot` may be undefined return snapshot?.context.count; }); count; // number | undefined对应源码中TActor extends Pick<AnyActorRef, ...> | undefined的泛型约束,以及if (!actor) return () => {};的空订阅分支(见 useSelector.ts)。
createActorContext:全局 Actor 的 React Context 集成
基本用法(3.1.0 引入)
createActorContext(logic, options?)返回一个绑定 Actor 的 React Context 对象,官方在 3.1.0 中明确其包含:
.Provider—— React Context Provider;.useActor(...)—— 获取当前状态并向 Actor 发送事件(v4 起从 context 上移除);.useSelector(...)—— 派生状态订阅;.useActorRef()—— 获取 Actor 引用。
import { createActorContext } from '@xstate/react'; import { someMachine } from './someMachine'; // Create a React Context object that will interpret the machine const SomeContext = createActorContext(someMachine); function SomeComponent() { // Get the current state and `send` function const [state, send] = SomeContext.useActor(); // Or select some derived state const someValue = SomeContext.useSelector((state) => state.context.someValue); // Or get a reference to the actor const actorRef = SomeContext.useActorRef(); return (/* ... */); } function App() { return ( <SomeContext.Provider> <SomeComponent /> </SomeContext.Provider> ); }从当前源码 createActorContext.ts 看,createActorContext返回的钩子已收敛为Provider、useActorRef(即内部useContext)与useSelector三个成员。
Provider 的 options 合并(4.0.3 / 4.0.0 / 3.2.0)
Provider组件接受options属性,其语义与useMachine(machine, options)的第二个参数一致。3.2.0 将 options 从createActorContext(machine, options)的第二个参数迁移到<Provider options={...}>上;4.0.3 进一步修复了options 合并问题——此前 Provider 的 options 会整体替换创建时的 options,现在二者正确合并:
const { inspect } = createBrowserInspector(); const SomeContext = createActorContext(someMachine, { inspect }); // ... // Options are now merged: // { inspect: inspect, input: 10 } <SomeContext.Provider options={{ input: 10 }}> {/* ... */} </SomeContext.Provider>;合并逻辑可见于 createActorContext.ts:useActorRef(providedLogic, { ...actorOptions, ...providedOptions }),先展开创建时 options,再覆盖 Provider 传入的 options。4.0.0 中还移除了createActorContext第三参数observerOrListener,并弃用了 Provider 上的machineprop(源码中会直接throw提示改用logic,见 createActorContext.ts)。
类型安全:types/input 定义后的必填约束(4.1.2)
在 4.1.2 之前,即便机器在setup({ types: { input } })中声明了input类型,useActor、useMachine、useActorRef也不会在编译期强制要求传入input,容易在运行时崩溃。此版本修复后,声明即必填:
const machine = setup({ types: { input: {} as { value: number } } }).createMachine({}); function App() { // With this change the above code will show a type error, // since `input` is now required: const _ = useMachine(machine, { input: { value: 1 } // Now input is required at compile time! }); return <></>; }这得益于 Hooks 签名中的工具类型ConditionalRequired与RequiredActorOptionsKeys:当泛型推导出 options 中存在必填键时,第二个参数会变为必填(见 useActor.ts 与 useActorRef.ts)。
4.0.0 破坏性变更与迁移清单
CHANGELOG 中 4.0.0(含 4.0.0-beta 系列)是 API 收敛最剧烈的一版,迁移时需逐条对照:
| 变更 | 旧写法 | 新写法 |
|---|---|---|
useMachine弃用为useActor别名 | useMachine(machine) | useActor(machine),或继续使用useMachine(等价) |
移除useSpawn | useSpawn(machine) | useActorRef(machine) |
移除useActor(actorRef)的旧用法 | const [state, send] = useActor(actorRef) | const state = useSelector(actorRef, s => s),发送用actorRef.send(...) |
| 实现(actions 等)不再作为 options 传入 | useMachine(machine, { actions: {...} }) | useMachine(machine.provide({ actions: {...} })) |
移除useMachine的工厂函数参数 | useMachine(() => createMachine(...)) | 直接传机器对象 |
移除 hooks 的getSnapshot参数 | useActor(actor, (actor) => actor.current) | 依赖 ActorRef 自带getSnapshot() |
| FSM 相关函数移除 | @xstate/react/fsm相关 | 使用统一的useActor |
Context 上移除useActor | MyCtx.useActor() | 用MyCtx.useSelector+MyCtx.useActorRef替代 |
关于machine.provide(...),官方特别说明:@xstate/react会检测机器的 config 是否仍相同,因此用provide后不会触发 "machine has changed" 警告。同时,移除工厂函数参数意味着机器实例应在组件外(或 memo 后)稳定创建,以避免每次渲染重新初始化。
exports字段也在 v4 中被加入package.jsonmanifest(见 package.json),限制了可从包导入的文件范围——只能导入公共 API,不能再从深层路径引入内部模块。
React 严格模式、Fast Refresh 与渲染稳定性
CHANGELOG 中多个版本都围绕 React 渲染语义修复问题,这些修复对生产环境的稳定性影响显著:
- 4.0.1:修复 React Strict Mode 下
after延迟转换失效的问题。此前严格模式下 effect 的重复挂载/卸载会破坏延迟事件调度,修复后after转换在所有 React 模式下都能按预期工作。 - 4.0.0 (Minor):Fast Refresh 在大多数场景下恢复正常工作,组件热更新后不再卡在过期的内部快照。
- 3.0.0:
useMachine在内部服务重启(如 Fast Refresh 场景)时会以初始状态正确重渲染,避免展示与真实服务不一致的陈旧状态。 - 3.0.0:
@xstate/fsm的useMachine改为在 effect 中启动服务,避免渲染期副作用并提升 StrictMode 兼容性;fsm 的实现更新放入 layout effect,规避布局 effect 发事件时的陈旧闭包问题。
支撑这些能力的底层是卸载清理函数stopRootWithRehydration(见 stopRootWithRehydration.ts):它会递归遍历 Actor 树持久化每个 Actor 的快照、清空 observers 后停止根 Actor,再恢复快照,从而在严格模式的 effect 重连中保持可预测行为。
版本演进中的其他重要节点
- 3.0.0:将 peer dependency 提升到 React 18,并基于
use-sync-external-store(含 shim)重写实现,从而兼容更老的 React 版本;同时移除asEffect与asLayoutEffectaction creators——官方建议在 effect 中直接执行副作用,或向机器发送事件后再由机器响应动作。 - 2.0.0:TypeScript 4.0+ 开始支持 typegen;移除已弃用的
useService,统一由useActor替代;Hooks 的泛型收敛为单一TMachine。 - 1.3.0:引入
useInterpret(低阶解释器 Hook,返回 service)与useSelector;useService宣布弃用。 - 1.1.0:spawned/invoked Actor 统一类型化为
ActorRef,配合ActorRefFrom<typeof machine>可让子组件安全接收 Actor 引用并保持类型完整(state推导为State<SomeContext, SomeEvent>,send只能发送合法事件)。 - 1.0.0-rc.7:
useMachine曾支持懒创建机器(useMachine(() => createMachine(...))),该能力在 v4 被移除。 - 0.7.0 / 0.7.1:早期允许将
guards、actions、activities、services、delays、updates等机器配置合并进useMachine(machine, options),且 action 实现会持续保持最新、不引用陈旧数据;这一设计最终被 v4 的machine.provide(...)取代。
各版本与核心包xstate的依赖关系同样记录在 CHANGELOG 中,例如 5.0.0 依赖xstate@5.19.0、4.1.3 依赖xstate@5.18.2、4.0.0 依赖xstate@5.20.0。从 5.0.3 / 5.0.0 起,React 19 被加入 peer dependency(见 package.json),当前包同时支持 React 18 与 19。
总结
通过 CHANGELOG 与源码的对照可以看到,@xstate/react的演进主线是:一切皆 Actor。从早期面向机器的useMachine/useInterpret,到面向任意逻辑的useActor/useActorRef/useSelector,再到全局化的createActorContext,包的设计始终围绕 ActorRef 的快照订阅与类型安全展开。升级时,优先对照本文的 4.0.0 迁移清单,把机器实现迁移到machine.provide(...)、把引用订阅改为useSelector、把创建引用改为useActorRef,即可平稳过渡到当前的 Actor 模型。
- 前端
- 后端
【免费下载链接】xstate
State machines, statecharts, and actors for complex logic
相关推荐
@xstate/react 实战指南:在 React 中用 useMachine 与 useSelector 集成 XState 状态机
@xstate/react 实战指南:在 React 中用 useMachine 与 useSelector 集成 XState 状态机 @xstate/rea
前端后端告别状态混乱:React状态机方案XState实战指南
告别状态混乱:React状态机方案XState实战指南 你是否还在为React应用中的状态管理头疼?表单验证逻辑混乱、用户交互状态失控、异步操作竞态条件频发?本
前端教程文档用 XState v5 与 React 实现 TodoMVC:从状态机设计到 localStorage 持久化的完整实践
用 XState v5 与 React 实现 TodoMVC:从状态机设计到 localStorage 持久化的完整实践 导读 本文围绕当前仓库中的 examp
前端后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考