Skip to content

从 XState v4 迁移到 v5

下面的指南解释了如何从 XState 版本 4 迁移到版本 5。从 XState v4 迁移到 v5 应该是一个简单的过程。如果您遇到困难或有任何问题,请通过 我们的 Discord 联系 Stately 团队。

本指南适用于希望将代码库从 v4 更新到 v5 的开发人员,也适用于任何希望了解 v4 和 v5 之间差异的开发人员。

XState v5 和 TypeScript

XState v5 及其相关库是用 TypeScript 编写的,并利用复杂的类型为您提供最佳的类型安全性和推断能力。XState v5 需要 TypeScript 版本 5.0 或更高版本。 为了获得最佳效果,请使用最新的 TypeScript 版本。

请遵循以下指南以确保您的 TypeScript 项目准备好使用 XState v5:

  • 使用最新版本的 TypeScript,版本 5.0 或更高(必需)

    npm install typescript@latest --save-dev
  • 在您的 tsconfig.json 文件中将 strictNullChecks 设置为 true。这将确保我们的类型正常工作,并有助于捕捉代码中的错误(强烈推荐)

    // tsconfig.json
    {
    "compilerOptions": {
    // ...
    "strictNullChecks": true
    // 或将 `strict` 设置为 true,这包括 `strictNullChecks`
    // "strict": true
    }
    }
  • 在您的 tsconfig.json 文件中将 skipLibCheck 设置为 true(推荐)

创建机器和演员

使用 createMachine(),而不是 Machine()

Machine(config) 函数现在称为 createMachine(config)

import { createMachine } from 'xstate';

const machine = createMachine({
// ...
});

使用 createActor(),而不是 interpret()

interpret() 函数已重命名为 createActor()

import { createMachine, createActor } from 'xstate';

const machine = createMachine(/* ... */);

// ✅
const actor = createActor(machine, {
// actor 选项
});

使用 machine.provide(),而不是 machine.withConfig()

machine.withConfig() 方法已重命名为 machine.provide()

// ✅
const specificMachine = machine.provide({
actions: {
/* ... */
},
guards: {
/* ... */
},
actors: {
/* ... */
},
// ...
});

使用 input 设置上下文,而不是 machine.withContext()

machine.withContext(...) 方法不能再使用,因为 context 不能再直接覆盖。请改用 input

// ✅
const machine = createMachine({
context: ({ input }) => ({
actualMoney: Math.min(input.money, 42),
}),
});

const actor = createActor(machine, {
input: {
money: 1000,
},
});

动作默认按顺序执行,不再需要 predictableActionArguments

动作现在默认按顺序执行,因此不再需要 predictableActionArguments 标志。分配动作将始终按定义的顺序运行。

// ✅
const machine = createMachine({
entry: [
({ context }) => {
console.log(context.count); // 0
},
assign({ count: 1 }),
({ context }) => {
console.log(context.count); // 1
},
assign({ count: 2 }),
({ context }) => {
console.log(context.count); // 2
},
],
});

spawn() 函数已被移除

不再使用导入的 spawn() 函数在 assign(...) 动作中创建演员:

  • 使用 spawnChild(...) 动作创建器(首选)
  • 或者在 assign(...) 动作中使用传递给分配函数的第一个参数中的 spawn(...) 方法(如果需要在 context 中使用演员引用)

阅读有关生成演员的文档以获取更多信息。

// ✅
import { spawnChild, assign } from 'xstate';

// 生成一个直接子演员:
const machine1 = createMachine({
// ...
entry: spawnChild('someChildLogic', {
id: 'someChild',
})
});

// 生成一个在 [`context`](command:_github.copilot.openSymbolFromReferences?%5B%22%22%2C%5B%7B%22uri%22%3A%7B%22scheme%22%3A%22file%22%2C%22authority%22%3A%22%22%2C%22path%22%3A%22%2FD%3A%2FDOC%2Fxstate-doc%2Fdocs%2Fmigration.mdx%22%2C%22query%22%3A%22%22%2C%22fragment%22%3A%22%22%7D%2C%22pos%22%3A%7B%22line%22%3A216%2C%22character%22%3A2%7D%7D%5D%2C%221e3bedd9-7dd0-41fd-9f83-d76e3d7b4562%22%5D "Go to definition") 中有演员引用的子演员:
const machine2 = createMachine({
// ...
entry: assign({
child: ({ spawn }) => spawn('someChildLogic')
})
});

使用 getNextSnapshot(…) 代替 machine.transition(…)

machine.transition(…) 方法现在需要一个“演员范围”作为第三个参数,该参数由 createActor(…) 内部创建。相反,使用 getNextSnapshot(…) 根据当前快照和事件从某些演员逻辑中获取下一个快照:

// ✅
import {
createMachine,
getNextSnapshot
} from 'xstate';

const machine = createMachine({
// ...
});

const nextState = getNextSnapshot(
machine,
machine.resolveState({ value: 'green' }),
{ type: 'timer' }
);

nextState.value; // yellow

显式发送事件而不是使用 autoForward

autoForward 属性在调用配置中已被移除。相反,请显式发送事件。

通常,不推荐将所有事件转发给演员。相反,只转发演员需要的特定事件。

// ✅
const machine = createMachine({
// ...
invoke: {
src: 'someSource',
id: 'someId'
},
always: {
// 将事件转发给被调用的演员
// 这在 XState v5 中不会导致无限循环
actions: sendTo('someId', ({ event }) => event)
}
});

状态

使用 state.getMeta() 代替 state.meta

state.meta 属性已重命名为 state.getMeta()

// ✅
state.getMeta();

state.toStrings() 方法已被移除

import { type StateValue } from 'xstate';

export function getStateValueStrings(stateValue: StateValue): string[] {
if (typeof stateValue === 'string') {
return [stateValue];
}
const valueKeys = Object.keys(stateValue);

return valueKeys.concat(
...valueKeys.map((key) =>
getStateValueStrings(stateValue[key]!).map((s) => key + '.' + s)
)
);
}

// ...

const stateValueStrings = getStateValueStrings(stateValue);
// 例如 ['green', 'yellow', 'red', 'red.walk', 'red.wait', …]

使用 state._nodes 代替 state.configuration

state.configuration 属性已重命名为 state._nodes

// ✅
state._nodes;

从检查 API 读取事件,而不是 state.events

state.events 属性已被移除,因为事件不是状态的一部分,除非您明确将它们添加到状态的 context 中。请使用 检查 API 来观察事件,或者将事件明确添加到状态的 context 中:

// ✅
import { createActor } from 'xstate';
import { someMachine } from './someMachine';

const actor = createActor(someMachine, {
inspect: (inspEvent) => {
if (inspEvent.type === '@xstate.event') {
console.log(inspEvent.event);
}
}
});

事件和转换

实现函数接收单个参数

实现函数现在接收一个单一参数:一个包含 contextevent 和其他属性的对象。

// ✅
const machine = createMachine({
entry: ({ context, event }) => {
// ...
},
});

send() 已被移除;请使用 raise()sendTo()

send(...) 动作创建器已被移除。请使用 raise(...) 发送事件给自己,或使用 sendTo(...) 发送事件给其他演员。

阅读有关 sendTo 动作raise 动作 的文档以获取更多信息。

// ✅
const machine = createMachine({
// ...
entry: [
// Send an event to self
raise({ type: 'someEvent' }),

// Send an event to another actor
sendTo('someActor', { type: 'someEvent' }),
],
});

迁移前提示: 更新 v4 项目以使用 sendToraise 代替 send

使用 enqueueActions() 代替 pure()choose()

pure()choose() 方法已被移除。请改用 enqueueActions()

对于 pure() 动作:

// ✅
entry: [
enqueueActions(({ context, event, enqueue }) => {
enqueue('action1');
enqueue('action2');
})
];

对于 choose() 动作:

// ✅
entry: [
enqueueActions(({ enqueue, check }) => {
if (check('someGuard')) {
enqueue('action1');
enqueue('action2');
}
})
];

actor.send() 不再接受字符串类型

字符串事件类型不再可以发送,例如 actor.send(event);您必须发送一个事件对象:

// ✅
actor.send({ type: 'someEvent' });

迁移前提示: 更新 v4 项目以传递对象给 .send()

state.can() 不再接受字符串类型

字符串事件类型不再可以发送,例如 state.can(event);您必须发送一个事件对象:

// ✅
state.can({ type: 'someEvent' });

受保护的转换使用 guard,而不是 cond

受保护转换的 cond 属性现在称为 guard

// ✅
const machine = createMachine({
on: {
someEvent: {
guard: 'someGuard',
target: 'someState',
},
},
});

使用 params 传递参数给动作和守卫

动作对象和守卫对象中除了 type 之外的属性应该嵌套在 params 属性下;{ type: 'someType', message: 'hello' } 变为 { type: 'someType', params: { message: 'hello' }}。这些 params 然后会作为动作或守卫实现的第二个参数传递:

// ✅
const machine = createMachine({
entry: {
type: 'greet',
params: {
message: 'Hello world',
},
},
on: {
someEvent: {
guard: { type: 'isGreaterThan', params: { value: 42 } },
},
},
}).provide({
actions: {
greet: ({ context, event }, params) => {
console.log(params.message); // 'Hello world'
},
},
guards: {
isGreaterThan: ({ context, event }, params) => {
return event.value > params.value;
},
},
});

迁移前提示: 更新 v4 项目中的动作和守卫对象,将 type 之外的属性移动到 params 对象中。

使用通配符 * 转换,而不是严格模式

严格模式已被移除。如果您希望在未处理的事件上抛出错误,您应该使用通配符转换:

// ✅
const machine = createMachine({
on: {
knownEvent: {
// ...
},
'*': {
// unknown event
actions: ({ event }) => {
throw new Error(`Unknown event: ${event.type}`);
},
},
},
});

const actor = createActor(machine);

actor.subscribe({
error: (err) => {
console.error(err);
}
});

actor.start();

actor.send({ type: 'unknownEvent' });

使用显式的无事件(always)转换

无事件(“always”)转换现在必须通过状态节点的 always: { ... } 属性定义;它们不能再通过空字符串定义:

// ✅
const machine = createMachine({
// ...
states: {
someState: {
always: {
target: 'anotherState',
},
},
},
});

迁移前提示: 更新 v4 项目以使用 always 进行无事件转换。

使用 reenter: true,而不是 internal: false

internal: false 现在是 reenter: true

以前用 internal: false 指定的外部转换现在用 reenter: true 指定:

// ✅
const machine = createMachine({
// ...
on: {
someEvent: {
target: 'sameState',
reenter: true,
},
},
});

转换默认是内部的,而不是外部的

所有转换默认是内部的。此更改与在状态节点上定义的转换相关,这些状态节点具有 entryexit 动作、调用的演员或延迟转换 (after)。如果您依赖于以前 XState v4 的行为,即转换隐式重新进入状态节点,请使用 reenter: true

// ✅
const machine = createMachine({
// ...
states: {
compoundState: {
entry: 'someAction',
on: {
someEvent: {
target: 'compoundState.childState',
// Reenters the `compoundState` state,
// just like an external transition
reenter: true,
},
selfEvent: {
target: 'childState',
reenter: true
}
},
initial: 'childState',
states: {
childState: {},
},
},
},
});
// ✅
const machine = createMachine({
// ...
states: {
compoundState: {
after: {
1000: {
target: 'compoundState.childState',
reenter: true, // make it external explicitly!
},
},
initial: 'childState',
states: {
childState: {},
},
},
},
});

子状态节点总是重新进入

当复合状态节点上定义的转换(无论是外部还是内部)目标是子状态节点时,子状态节点总是重新进入。这一变化仅在子状态节点具有 entryexit 动作、调用的演员或延迟转换 (after) 时相关。添加 stateIn 守卫以防止子状态的非预期重新进入:

// ✅

const machine = createMachine({
// ...
states: {
compoundState: {
on: {
someEvent: {
guard: not(stateIn({ compoundState: 'childState' })),
target: '.childState',
},
},
initial: 'childState',
states: {
childState: {
entry: 'someAction',
},
},
},
},
})

使用 stateIn() 验证状态转换,而不是 in

in: 'someState' 转换属性已被移除。请改用 guard: stateIn(...)

// ✅
const machine = createMachine({
on: {
someEvent: {
guard: stateIn({ form: 'submitting' }),
target: 'someState',
},
},
});

使用 actor.subscribe() 代替 state.history

state.history 属性已被移除。如果您需要前一个快照,应该通过 actor.subscribe(...) 来维护。

// ✅
let previousSnapshot = actor.getSnapshot();

actor.subscribe((snapshot) => {
doSomeComparison(previousSnapshot, snapshot);
previousSnapshot = snapshot;
});

迁移前提示: 更新 v4 项目以使用 actor.subscribe() 跟踪历史记录。

动作可以抛出错误而无需 escalate

escalate 动作创建器已被移除。在 XState v5 中,动作可以抛出错误,并且它们将按预期传播。错误可以使用 onError 转换进行处理。

// ✅
const childMachine = createMachine({
// 这将发送到调用此子机器的父机器
entry: () => {
throw new Error('This is some error')
}
});

const parentMachine = createMachine({
invoke: {
src: childMachine,
onError: {
actions: ({ context, event }) => {
console.log(event.error);
// {
// type: ...,
// error: {
// message: 'This is some error'
// }
// }
}
}
}
});

Actors

使用 actor 逻辑创建器代替函数作为 invoke.src

可用的 actor 逻辑创建器有:

  • createMachine
  • fromPromise
  • fromObservable
  • fromEventObservable
  • fromTransition
  • fromCallback

有关更多信息,请参阅 Actors

// ✅
import { fromPromise, setup } from 'xstate';

const machine = setup({
actors: {
getUser: fromPromise(async ({ input }: { input: { userId: string } }) => {
const data = await getData(input.userId);
// ...
return data;
})
}
}).createMachine({
invoke: {
src: 'getUser',
input: ({ context, event }) => ({
userId: context.userId,
}),
},
});
// ✅
import { fromCallback, createMachine } from 'xstate';

const machine = createMachine({
invoke: {
src: fromCallback(({ sendBack, receive, input }) => {
// ...
}),
input: ({ context, event }) => ({
userId: context.userId,
}),
},
});
// ✅
import { fromEventObservable, createMachine } from 'xstate';
import { interval, mapTo } from 'rxjs';

const machine = createMachine({
invoke: {
src: fromEventObservable(() =>
interval(1000).pipe(mapTo({ type: 'tick' })),
),
},
});

使用 invoke.input 代替 invoke.data

invoke.data 属性已被移除。如果您想为被调用的演员提供上下文,请使用 invoke.input

// ✅
const someActor = createMachine({
// The input must be consumed by the invoked actor:
context: ({ input }) => input,
// ...
});

const machine = createMachine({
// ...
invoke: {
src: 'someActor',
input: {
value: 42,
},
},
});

在最终状态中使用 output 代替 data

要从达到最终状态的机器生成输出数据,请使用顶级 output 属性代替 data

// ✅
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
},
},
output: {
answer: 42,
},
});

要提供动态生成的输出,请将 invoke.data 替换为 invoke.output 并添加顶级 output 属性:

// ✅
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
output: ({ event }) => ({
answer: event.someValue,
}),
},
},
output: ({ event }) => event.output,
});

不要在 inputoutput 中使用属性映射器

如果您想为被调用的演员提供动态上下文,或从最终状态生成动态输出,请使用函数而不是带有属性映射器的对象。

// ✅
const machine = createMachine({
// ...
invoke: {
src: 'someActor',
input: ({ context, event }) => ({
value: event.value,
}),
},
});

// 被调用的 actor 必须消费 input:
const someActor = createMachine({
// ...
context: ({ input }) => input,
});

// 生成机器输出
const machine = createMachine({
// ...
states: {
finished: {
type: 'final',
},
},
output: ({ context, event }) => ({
answer: context.value,
}),
});

使用 options 对象上的 actors 属性代替 services

services 已重命名为 actors

// ✅
const specificMachine = machine.provide({
actions: {
/* ... */
},
guards: {
/* ... */
},
actors: {
/* ... */
},
// ...
});

使用 subscribe() 监听变化,而不是 onTransition()

actor.onTransition(...) 方法已被移除。请改用 actor.subscribe(...)

// ✅
const actor = createActor(machine);
actor.subscribe((state) => {
// ...
});

createActor()(原 interpret())接受第二个参数来恢复状态

interpret(machine).start(state) 现在是 createActor(machine, { snapshot }).start()

要在特定状态下恢复演员,现在应将状态作为 createActor(logic, options)options 参数的 snapshot 属性传递。actor.start() 方法不再接受 state 参数。

// ✅
const actor = createActor(machine, { snapshot: someState });
actor.start();

使用 actor.getSnapshot() 获取演员的状态

在演员启动后订阅演员 (actor.subscribe(...)) 将不再立即发出当前快照。相反,请从 actor.getSnapshot() 读取当前快照:

// ✅
const actor = createActor(machine);
actor.start();

const initialState = actor.getSnapshot();

actor.subscribe((state) => {
// Snapshots from when the subscription was created
// Will not emit the current snapshot until a transition happens
});

遍历事件而不是使用 actor.batch()

用于批量处理事件的 actor.batch([...]) 方法已被移除。

// ✅
for (const event of events) {
actor.send(event);
}

迁移前提示: 更新 v4 项目以循环遍历事件并将它们作为批处理发送。

使用 snapshot.status === 'done' 代替 snapshot.done

snapshot.done 属性已被移除,该属性以前在状态机演员的快照对象中。请改用 snapshot.status === 'done',它适用于所有演员:

// ✅
const actor = createActor(machine);
actor.start();

actor.subscribe((snapshot) => {
if (snapshot.status === 'done') {
// ...
}
});

state.nextEvents 已被移除

state.nextEvents 属性已被移除,因为它不是确定可以发送给演员的下一个事件的完全安全/可靠的方法。如果您想根据以前的行为获取下一个事件,可以使用此辅助函数:

import type { AnyMachineSnapshot } from 'xstate';

function getNextEvents(snapshot: AnyMachineSnapshot) {
return [...new Set([...snapshot._nodes.flatMap((sn) => sn.ownEvents)])];
}

// 代替 `state.nextEvents`:
const nextEvents = getNextEvents(state);

TypeScript

使用 types 代替 schema

machineConfig.schema 属性已重命名为 machineConfig.types

// ✅
const machine = createMachine({
types: {} as {
context: {
/* ...*/
};
events: {
/* ...*/
};
},
});

使用 types.typegen 代替 tsTypes

machineConfig.tsTypes 属性已被重命名,现在位于 machineConfig.types.typegen

// ✅
const machine = createMachine({
types: {} as {
typegen: {};
context: {
/* ...*/
};
events: {
/* ...*/
};
},
});

@xstate/react

useInterpret() 现在是 useActorRef()

useInterpret() 钩子(用于返回 actorRef,在 XState v4 中称为 "service")已重命名为 useActorRef()

// ✅
import { useActorRef } from '@xstate/react';

const actorRef = useActorRef(machine); // or any other logic

useActor(logic) 现在接受 actor 逻辑,而不是 actor

useActor(logic) 钩子现在接受 actor 逻辑(例如 fromPromise(...)createMachine(...) 等)而不是现有的 ActorRef

要使用现有的 ActorRef,请使用 actor.send(...) 发送事件,并使用 useSelector(actor, ...) 获取快照:

// ✅
import { useSelector } from '@xstate/react';

function Component({ someActorRef }) {
const state = useSelector(someActorRef, (s) => s);

return (
<button onClick={() => someActorRef.send({ type: 'someEvent' })} />
);
}

使用 machine.provide() 在钩子中提供实现

对于动态创建具有提供实现的机器,useMachine(...)useActor(...)useActorRef(...) 钩子不再接受:

  • 作为第一个参数的惰性机器创建器
  • 传递给第二个参数的实现

相反,machine.provide(...) 应直接传递给第一个参数。

@xstate/react 包将具有相同配置的机器视为相同的机器,因此它将最小化重新渲染,但仍会保持提供的实现是最新的。

// ✅
import { useMachine } from '@xstate/react';
import { someMachine } from './someMachine';

function Component(props) {
const [state, send] = useMachine(someMachine.provide({
actions: {
doSomething: () => {
props.onSomething?.(); // Kept up-to-date
}
}
}));

// ...
}

@xstate/vue

useMachine() 现在返回 snapshot 而不是 state,并返回 actor 而不是 service

为了与 XState 和相关库保持一致的命名:

  • state 现在是 snapshot
  • service 现在是 actor
// ✅
import { useMachine } from '@xstate/vue';

// ...

const {
snapshot, // Renamed from `state`
send,
actor // Renamed from `service`
} = useMachine(someMachine)

新功能

常见问题

Stately Studio 何时会兼容 XState v5?

我们目前正在努力使 Stately Studio 兼容 XState v5。导出到 XState v5(JavaScript 或 TypeScript)已经可用。我们正在努力支持 XState v5 的新功能,例如高阶守卫、部分事件通配符和机器输入/输出。

在我们的路线图上为 Stately Studio + XState v5 兼容性 点赞或发表评论,以随时了解我们的进展。

XState VS Code 扩展何时会兼容 XState v5?

XState VS Code 扩展 尚未兼容 XState v5。该扩展是我们的优先事项,相关工作已经在进行中。

在我们的路线图上为 VS Code 扩展的 XState v5 兼容性 点赞或发表评论,以随时了解我们的进展。

XState v5 何时会有 typegen?

XState v5 中的 TypeScript 推断功能得到了极大改进。特别是通过 setup() API 和动态参数等功能,主要的 typegen 用例不再需要。

然而,我们认识到可能仍然存在一些特定的 typegen 用例。在我们的路线图上为 XState v5 的 typegen 点赞或发表评论,以随时了解我们的进展。

如何同时使用 XState v4 和 v5?

您可以在同一个项目中同时使用 XState v4 和 v5,这对于逐步迁移到 XState v5 非常有用。要同时使用这两个版本,请手动或通过 CLI 将 "xstate5": "npm:xstate@5" 添加到您的 package.json 中:

npm i xstate5@npm:xstate@5

然后,您可以在代码中导入 XState v5 版本:

import { createMachine } from 'xstate5';
// or { createMachine as createMachine5 } from 'xstate5';

如果您需要使用不同版本的集成包,例如 @xstate/react,您可以使用类似上面的策略,但需要在集成包中链接到正确版本的 XState。这可以通过使用脚本来完成:

npm i xstate5@npm:xstate@5 @xstate5/react@npm:@xstate/react@4 --force
// scripts/xstate5-react-script.js
const fs = require('fs-extra');
const path = require('path');

const rootNodeModules = path.join(__dirname, '..', 'node_modules');

fs.ensureSymlinkSync(
path.join(rootNodeModules, 'xstate5'),
path.join(rootNodeModules, '@xstate5', 'react', 'node_modules', 'xstate'),
);
// package.json
"scripts": {
"postinstall": "node scripts/xstate5-react-script.js"
}

然后,您可以在代码中使用兼容 XState v5 的 @xstate/react 版本:

import { useMachine } from '@xstate5/react';
// or { useMachine as useMachine5 } from '@xstate5/react';
import { createMachine } from 'xstate5';
// or { createMachine as createMachine5 } from 'xstate5';

// ...