Skip to content

@xstate/store

XState Store 是一个用于在 JavaScript/TypeScript 应用程序中进行简单状态管理的小型库。它旨在通过 事件 更新存储数据,适用于纯 JavaScript/TypeScript 应用程序、React 应用程序等。它类似于 Zustand、Redux 和 Pinia 等库。对于更复杂的状态管理,您应该使用 XState,或者您可以 将 XState Store 与 XState 一起使用

安装

npm install @xstate/store

快速开始

import { createStore } from '@xstate/store';

const store = createStore({
// 初始上下文
context: { count: 0, name: 'David' },
// 转换
on: {
inc: {
count: (context) => context.count + 1,
},
add: {
count: (context, event: { num: number }) => context.count + event.num,
},
changeName: {
name: (context, event: { newName: string }) => event.newName,
},
},
});

// 获取当前状态(快照)
console.log(store.getSnapshot());
// => {
// status: 'active',
// context: { count: 0, name: 'David' }
// }

// 订阅快照变化
store.subscribe((snapshot) => {
console.log(snapshot.context);
});

// 发送事件
store.send({ type: 'inc' });
// 输出 { count: 1, name: 'David' }

store.send({ type: 'add', num: 10 });
// 输出 { count: 11, name: 'David' }

store.send({ type: 'changeName', newName: 'Jenny' });
// 输出 { count: 11, name: 'Jenny' }

创建一个 store

要创建一个 store,您需要将一个对象传递给 createStore(…) 函数,该对象具有以下属性:

  1. 初始 context
  2. 一个 on 对象用于转换(事件处理程序),其中:
  • 键是事件类型(例如 "inc""add""changeName"
  • 值是发送事件到 store 时要应用的 context 更新,可以是对象或函数。

在转换中更新 context 类似于在 XState 中使用 assign 动作。您可以使用对象更新特定的 context 属性:

import { createStore } from '@xstate/store';

const store = createStore({
context: { count: 0, incremented: false /* ... */ },
on: {
inc: {
count: (context, event: { by: number }) => context.count + event.by,
// 静态值不需要包裹在函数中
incremented: true,
},
},
});

或者,您可以使用函数更新整个 context

import { createStore } from '@xstate/store';

const store = createStore({
context: { count: 0, incremented: false /* ... */ },
on: {
inc: (context, event: { by: number }) => {
// ...

return {
count: context.count + event.by,
incremented: true,
};
},
},
});

您可以在使用函数更新整个 context 时展开 ...context。这在您希望保留 context 中的其他属性时非常有用:

import { createStore } from '@xstate/store';

const store = createStore({
context: { count: 0, incremented: false /* ... */ },
on: {
reset: (context, event) => {
// 您可以使用 `...context` 来保留其他属性
return {
...context,
count: 0,
};
},
},
});

注意:已弃用的 createStore(context, transitions) API

以前版本的 createStore 接受两个参数:初始上下文和事件处理程序对象。此 API 仍然受支持,但已弃用。以下是旧用法的示例:

import { createStore } from '@xstate/store';

const donutStore = createStore(
{
donuts: 0,
favoriteFlavor: 'chocolate',
},
{
addDonut: (context) => ({ ...context, donuts: context.donuts + 1 }),
changeFlavor: (context, event: { flavor: string }) => ({
...context,
favoriteFlavor: event.flavor,
}),
eatAllDonuts: (context) => ({ ...context, donuts: 0 }),
},
);

我们建议使用新的 API 以获得更好的类型推断和更明确的配置。

转换函数

转换函数是一个接受当前 contextevent 对象的函数,并返回:

  • 要更新的部分或整个 context 对象(如果使用函数赋值器)
  • 要更新的 context 属性值(如果使用对象赋值器)

为了强类型,您应该在转换函数中指定 event 对象的负载类型。

import { createStore } from '@xstate/store';

const store = createStore({
context: { name: 'David', count: 0 },
on: {
updateName: (context, event: { name: string }) => {
return {
name: event.name,
};
},
inc: {
count: (context, event: { by: number }) => {
return context.count + event.by;
},
},
},
});

store.send({
type: 'updateName',
name: 'Jenny', // 强类型为 `string`
});

store.send({
type: 'inc',
by: 10, // 强类型为 `number`
});

触发事件

您可以通过在转换函数的第三个参数中使用 emit 方法来触发事件:

import { createStore } from '@xstate/store';

const store = createStore({
types: {
emitted: {} as { type: 'incremented'; by: number },
},
context: { count: 0 },
on: {
inc: (context, event: { by: number }, { emit }) => {
if (event.by > 0) {
emit({ type: 'incremented', by: event.by });
}

return {
count: context.count + event.by,
};
},
},
});

const sub = store.on('incremented', (event) => {
console.log(`Emitted by ${event.by}`);
// => 输出 "Emitted by 10"
});

store.send({ type: 'inc', by: 10 });

// 停止监听发出的事件
sub.unsubscribe();

您可以使用 store.on(...) 方法监听发出的事件,该方法会创建一个订阅,您可以稍后取消订阅。此方法是类型安全的,确保您接收到的事件对象是您正在监听的发出事件类型的正确对象。

请注意,您可以在 store 配置对象的 types.emitted 属性中强类型发出的事件,就像在 XState 中一样。这确保了在发出和监听事件时的类型安全。

检查

就像在 XState 中一样,您可以使用 Inspect API 通过 .inspect 方法检查发送到 store 的事件和 store 内的状态转换:

const store = createStore({
// ...
});

store.inspect((inspectionEvent) => {
// 类型: '@xstate.snapshot' 或
// 类型: '@xstate.event'
console.log(inspectionEvent);
});

.inspect(…) 方法返回一个订阅对象:

const sub = store.inspect((inspectionEvent) => {
console.log(inspectionEvent);
});

// 停止监听检查事件
sub.unsubscribe();

您可以使用 Stately Inspector 来检查和可视化 store 的状态。

import { createBrowserInspector } from '@statelyai/inspect';
import { createStore } from '@xstate/store';

const store = createStore({
// ...
});

const inspector = createBrowserInspector({
// ...
});

store.inspect(inspector);

使用 Immer

如果您想使用 Immer 来更新 context,可以通过将 produce 函数作为第一个参数传递给 createStoreWithProducer(producer, …) 来实现。

import { createStoreWithProducer } from '@xstate/store';
import { produce } from 'immer';

const store = createStoreWithProducer(
// 生产者
produce,
{
context: { count: 0, todos: [] },
on: {
inc: (context, event: { by: number }) => {
// 无需返回值;由 Immer 处理
context.count += event.by;
},
addTodo: (context, event: { todo: string }) => {
// 无需返回值;由 Immer 处理
context.todos.push(event.todo);
},
},
},
);

// ...

请注意,当使用 createStoreFromProducer(…) 时,您不能使用对象赋值器语法,也没有必要。

在 React 中使用

如果您使用 React,可以使用 useSelector(store, selector) 钩子订阅 store 并获取当前状态。

import { createStore } from '@xstate/store';
import { useSelector } from '@xstate/store/react';

// 创建一个 store
const store = createStore({
context: { count: 0, name: 'David' },
on: {
inc: {
count: (context) => context.count + 1,
},
},
});

// 使用 `useSelector` 钩子订阅 store
function Component(props) {
const count = useSelector(store, (state) => state.context.count);

// 该组件显示计数并有一个按钮来增加计数
return (
<div>
// highlight-start Count: {count}
<button onClick={() => store.send({ type: 'inc' })}>Increment</button>
</div>
);
}

一个 store 可以与多个组件共享,这些组件将从 store 实例接收相同的快照。Stores 对于全局状态管理非常有用。

与 Solid 一起使用

文档即将推出!

将 XState Store 与 XState 一起使用

您可能会注意到 stores 与 XState 中的 actors 非常相似。这是有意为之的。XState 的 actors 非常强大,但对于简单的用例可能过于复杂,这就是 @xstate/store 存在的原因。

然而,如果您有现有的 XState 代码,并且喜欢使用 @xstate/store 创建 store 逻辑的简便性,您可以使用 fromStore(context, transitions) actor 逻辑创建器来创建 XState 兼容的 store 逻辑,并将其传递给 createActor(storeLogic) 函数:

import { fromStore } from '@xstate/store';
import { createActor } from 'xstate';

// 替换为:
// const store = createStore( ... };
const storeLogic = fromStore({
context: { count: 0, incremented: false /* ... */ },
on: {
inc: {
count: (context, event) => context.count + 1,
// 静态值不需要包裹在函数中
incremented: true,
},
},
});

const store = createActor(storeLogic);
store.subscribe((snapshot) => {
console.log(snapshot);
});
store.start();

store.send({
type: 'inc',
});

简而言之,您可以通过更改一行代码将 createStore(…) 转换为 fromStore(…)。请注意,fromStore(…) 返回的是 store 逻辑,而不是 store actor 实例。Store 逻辑传递给 createActor(storeLogic) 以创建 store actor 实例:

// 替换为:
// const store = createStore({
const storeLogic = fromStore({
context: {
// ...
},
on: {
// ...
},
});

// 创建 store(actor)
const storeActor = createActor(storeLogic);

使用 fromStore(…) 创建 store actor 逻辑的另一个优点是允许您通过使用接受 input 并返回初始 context 的上下文函数来提供 input

import { fromStore } from '@xstate/store';

const storeLogic = fromStore({
context: (initialCount: number) => ({
count: initialCount,
}),
on: {
// ...
},
});

const actor = createActor(storeLogic, {
input: 42,
});

将 stores 转换为状态机

如果您有一个 store 想要转换为 XState 中的状态机,可以通过以下方式进行转换:

  1. 使用 createMachine(…)(从 xstate 导入)代替 createStore(…)(从 @xstate/store 导入)来创建状态机。
  2. 将赋值操作包装在 assign(…) 动作创建器中(从 xstate 导入),并将其移动到转换的 actions 属性中。
  3. 从第一个参数中解构 contextevent,而不是将它们作为单独的参数。

例如,这是转换前的 store:

import { createMachine } from 'xstate';

// 1. 使用 `createMachine(…)` 代替 `createStore(…)`
const store = createStore({
context: { count: 0, name: 'David' },
on: {
inc: {
// 2. 将赋值操作包装在 `assign(…)` 中
// 3. 从第一个参数中解构 `context` 和 `event`
count: (context, event: { by: number }) => context.count + event.by,
},
},
});

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

以下是转换后的状态机:

import { createMachine } from 'xstate';

// const store = createStore(
// { count: 0, name: 'David' },
// {
// inc: {
// count: (context, event: { by: number }) => context.count + event.by
// }
// });

// 1. 使用 `createMachine(…)` 代替 `createStore(…)`
const machine = createMachine({
context: {
count: 0,
name: 'David',
},
on: {
inc: {
// 2. 将赋值操作包装在 `assign(…)` 中
actions: assign({
// 3. 从第一个参数中解构 `context` 和 `event`
count: ({ context, event }) => context.count + event.by,
}),
},
},
});

为了更强的类型检查,请使用 setup(…) 函数 来强类型化 contextevents

import { setup } from 'xstate';

const machine = setup({
types: {
context: {} as { count: number; name: string },
events: {} as { type: 'inc'; by: number },
},
}).createMachine({
// 与前面的示例相同
});

比较

本节将 XState Store 与其他流行的 TypeScript 状态管理库进行比较。此比较仅供参考,并不旨在偏袒任何一种方法。示例代码摘自 Zustand 的比较文档

与 Zustand 的比较

Zustand

import { create } from 'zustand';

type State = {
count: number;
};

type Actions = {
increment: (qty: number) => void;
decrement: (qty: number) => void;
};

const useCountStore = create<State & Actions>((set) => ({
count: 0,
increment: (qty: number) =>
set((state) => ({
count: state.count + qty,
})),
decrement: (qty: number) =>
set((state) => ({
count: state.count - qty,
})),
}));

const Component = () => {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
// ...
};

XState Store

import { createStore } from '@xstate/store';
import { useSelector } from '@xstate/store/react';

const store = createStore({
context: {
count: 0,
},
on: {
increment: (context, { qty }: { qty: number }) => ({
count: context.count + qty,
}),
decrement: (context, { qty }: { qty: number }) => ({
count: context.count - qty,
}),
},
});

const Component = () => {
const count = useSelector(store, (state) => state.context.count);
const increment = (qty) => store.send({ type: 'increment', qty });
const decrement = (qty) => store.send({ type: 'decrement', qty });
// ...
};