news 2026/9/30 8:14:04

@xstate/react 核心 API 演进指南:从 useMachine 到 useActor 与 ActorRef 的 React 状态机实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@xstate/react 核心 API 演进指南:从 useMachine 到 useActor 与 ActorRef 的 React 状态机实践
  • 前端
  • 后端

【免费下载链接】xstate

State machines, statecharts, and actors for complex logic

项目地址:https://gitcode.com/gh_mirrors/xs/xstate
点击查看免费下载

@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 渲染循环:

  1. 通过useIdleActorRef(logic, options)惰性创建 ActorRef(首次渲染时才createActor,见 useActorRef.ts);
  2. 用useSyncExternalStore订阅 Actor 的快照,subscribe与getSnapshot均用useCallback稳定引用;
  3. 在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(等价)
移除useSpawnuseSpawn(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 上移除useActorMyCtx.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

项目地址:https://gitcode.com/gh_mirrors/xs/xstate
点击查看免费下载
上一篇:Cloudflare Agents 中的 Codemode 实战:让 LLM 编写代码来编排你的 AI 工具
下一篇:LeetCode 277 名人问题(Find the Celebrity)全解:暴力枚举、O(n) 逻辑排除与记忆化缓存

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:13:36

MATLAB/Simulink配电网潮流仿真:IEEE 13节点馈线建模实战

一看这个标题&#xff0c;就知道是同道中人。配电网潮流计算和输电网完全是两码事&#xff0c;能在MATLAB/Simulink里把IEEE 13节点馈线跑明白&#xff0c;那对三相不平衡系统、电压调节器、恒功率负荷这些配电网核心概念&#xff0c;基本就摸到门道了。网上关于这个题材的资料…

作者头像 李华
网站建设 2026/9/30 8:13:23

IndexScan比SeqScan结果少?先排查这5类原因再决定重建索引

先别急着重建索引&#xff0c;也别急着回一句“索引坏了&#xff0c;reindex 吧”。我接到过不下十次这种求助&#xff0c;最后真正需要重建索引的不到一成。前两天同事火急火燎跑过来&#xff0c;给我看两条执行计划&#xff1a;同一张订单表&#xff0c;同一个 SQL 条件&…

作者头像 李华
网站建设 2026/9/30 8:13:14

Hindsight实战:Agent经验沉淀与记忆分层架构设计

1. 从“hindsight”这个词说起&#xff1a;为什么它值得单独拿出来聊 第一次看到“hindsight”作为项目标题&#xff0c;我脑子里蹦出来的不是某个具体工具&#xff0c;而是一个很朴素的场景&#xff1a;你在跟一个 AI Agent 协作&#xff0c;它前面明明已经确认过“这个项目用…

作者头像 李华
网站建设 2026/9/30 8:13:07

跨模态开发实践:用DeepSeek实现视频内容自动生成技术文档

简介&#xff1a;这份PDF文档面向希望将DeepSeek应用于跨模态开发的开发者与研究者&#xff0c;聚焦视频内容自动生成技术文档这一具体场景&#xff0c;帮助读者打通从文本、图像到视频的跨模态处理链路。文档共37页&#xff0c;以1个PDF文件交付&#xff0c;压缩包约2.07MB&am…

作者头像 李华
网站建设 2026/9/30 8:12:52

4步解除群晖NAS硬盘兼容性限制:Synology_HDD_db 完整上手指南

4步解除群晖NAS硬盘兼容性限制&#xff1a;Synology_HDD_db 完整上手指南 【免费下载链接】Synology_HDD_db Add your HDD, SSD and NVMe drives to your Synologys compatible drive database and a lot more 项目地址: https://gitcode.com/GitHub_Trending/sy/Synology_HD…

作者头像 李华
网站建设 2026/9/30 8:12:14

港股增发摊薄与30%强制要约:德祥引入瑞凯的资本运作拆解

最近德祥地产再披露增发方案的公告&#xff0c;我把几百字的公告翻来覆去看了几遍。里面最扎眼的是战略投资者瑞凯集团的持股比例。公告明确预计&#xff0c;这轮增发完成后&#xff0c;瑞凯的持股要走到30.9%。按港股老司机的说法&#xff0c;这个比例一过去&#xff0c;事情的…

作者头像 李华