@xstate/store
XState Store 是一个用于在 JavaScript/TypeScript 应用程序中进行简单状态管理的小型库。它旨在通过 事件 更新存储数据,适用于纯 JavaScript/TypeScript 应用程序、React 应用程序等。它类似于 Zustand、Redux 和 Pinia 等库。对于更复杂的状态管理,您应该使用 XState,或者您可以 将 XState Store 与 XState 一起使用。
安装
- npm
- pnpm
- yarn
npm install @xstate/store
pnpm install @xstate/store
yarn add @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(…)
函数,该对象具有以下属性:
- 初始
context
- 一个
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(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 以获得更好的类型推断和更明确的配置。
转换函数
转换函数是一个接受当前 context
和 event
对象的函数,并返回:
- 要更新的部分或整个
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 中的状态机,可以通过以下方式进行转换:
- 使用
createMachine(…)
(从xstate
导入)代替createStore(…)
(从@xstate/store
导入)来创建状态机。 - 将赋值操作包装在
assign(…)
动作创建器中(从xstate
导入),并将其移动到转换的actions
属性中。 - 从第一个参数中解构
context
和event
,而不是将它们作为单独的参数。
例如,这是转换前的 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(…)
函数 来强类型化 context
和 events
:
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 });
// ...
};