mcpField
X
xstate

xstate

github.com/pedronauck开发648239 次安装2 天前更新
代码仓库

关于 xstate

面向 XState v5 开发者的实践指南,涵盖状态机、@xstate/store 及与 TanStack Query 的协作模式,并提供类型安全与 React 集成建议。

主要功能

  1. 01状态机与 Actor 建模
  2. 02@xstate/store 事件驱动状态管理
  3. 03XState 与 TanStack Query 协作
  4. 04React 集成模式
  5. 05常见问题与验证清单

适用场景

  1. 01构建多步骤应用流程
  2. 02实现轻量事件驱动状态
  3. 03协调数据获取与界面状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pedronauck/skills --skill xstate

安装后在 Claude Code 中输入/xstate确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
machine-patterns.md
query-patterns.md
store-patterns.md

详细介绍

XState v5 开发指南

本技能为在此项目中使用 XState v5 生态系统提供全面的指南、模式和最佳实践,包括:

  • XState 状态机 - 用于复杂应用逻辑的有限状态机和 Actor
  • @xstate/store - 用于轻量级状态管理的简单事件驱动型存储
  • XState + TanStack Query - 数据获取编排的集成模式

快速参考

用例工具参考
复杂状态流、守卫、层级状态XState 状态机references/machine-patterns.md
简单的事件驱动状态、原子、撤销/重做@xstate/storereferences/store-patterns.md
通过状态编排进行数据获取XState + TanStack Queryreferences/query-patterns.md

核心原则

XState 状态机
  • 显式状态:将应用逻辑建模为明确的有限状态和事件
  • Actor 模型:在复杂编排中采用 Actor 模型
  • 消除不可能的状态:使用状态图消除不可能的状态
  • 声明式思维:以声明式方式思考状态转换
  • 可视化:使用 Stately Editor 以可视化方式创建和调试状态机
@xstate/store
  • 事件驱动更新:使用事件进行所有状态更新(绝不直接修改)
  • 保持存储简单:专注于数据管理,而非复杂逻辑
  • 优化订阅:利用选择器高效访问状态
  • 响应式状态:使用原子构建可组合的响应式状态
  • 升级路径:随着复杂度增加,迁移到 XState 状态机
XState + TanStack Query 集成
  • 使用 XState 进行编排:使用 XState 状态机编排工作流
  • 使用 TanStack Query 处理数据:使用 TanStack Query 获取和缓存数据
  • Actor 作为桥梁:将 Query 操作封装为 XState Actor
  • 在变更中更新缓存:将 Query 缓存更新放在变更回调中

XState 状态机

使用 setup() API 创建状态机

始终使用 setup() API 创建类型安全的状态机:

import { setup, createActor } from 'xstate';

export enum CounterEventType {
  INCREMENT = 'increment',
  DECREMENT = 'decrement',
}

const counterMachineSetup = setup({
  types: {
    context: {} as { count: number },
    events: {} as
      | { type: CounterEventType.INCREMENT }
      | { type: CounterEventType.DECREMENT },
    input: {} as { initialCount: number },
  },
  guards: {
    isPositive: ({ context }) => context.count > 0,
  },
});

// Define actions OUTSIDE setup using type-bound helpers
const incrementCount = counterMachineSetup.assign({
  count: ({ context }) => context.count + 1,
});

export const counterMachine = counterMachineSetup.createMachine({
  id: 'counter',
  context: ({ input }) => ({ count: input.initialCount }),
  initial: 'active',
  states: {
    active: {
      on: {
        [CounterEventType.INCREMENT]: {
          actions: incrementCount,
        },
      },
    },
  },
});
React 集成

组件级状态机使用 useMachine(),覆盖操作使用 .provide():

import { useMachine } from '@xstate/react';
import { useMemo } from 'react';

function Counter({ onExternalAction }) {
  const machine = useMemo(
    () =>
      counterMachine.provide({
        actions: {
          logEvent: () => onExternalAction(),
        },
      }),
    [onExternalAction]
  );

  const [state, send] = useMachine(machine);

  return (
    <button onClick={() => send({ type: CounterEventType.INCREMENT })}>
      Count: {state.context.count}
    </button>
  );
}

详细模式:请参阅 references/machine-patterns.md


@xstate/store

创建存储

通过明确的初始上下文和基于事件的转换来定义存储:

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

const userStore = createStore({
  context: {
    user: null as { id: string; name: string } | null,
    isLoading: false,
    error: null as string | null,
  },
  on: {
    setUser: (context, event: { user: { id: string; name: string } }) => ({
      ...context, // Always spread to preserve other properties
      user: event.user,
      error: null,
    }),
    setLoading: (context, event: { isLoading: boolean }) => ({
      ...context,
      isLoading: event.isLoading,
    }),
  },
});

// Use the trigger API for type-safe event sending
userStore.trigger.setUser({ user: { id: '1', name: 'John' } });
React 集成
import { useSelector, useStore } from '@xstate/store/react';

function Counter() {
  const store = useStore({
    context: { count: 0 },
    on: {
      increment: (context, event: { by: number }) => ({
        ...context,
        count: context.count + event.by,
      }),
    },
  });
  const count = useSelector(store, (state) => state.context.count);
  return <button onClick={() => store.trigger.increment({ by: 1 })}>{count}</button>;
}
撤销/重做模式
import { createStore } from '@xstate/store';
import { undoRedo } from '@xstate/store/undo';

const editorStore = createStore(
  undoRedo({
    context: { text: '' },
    on: {
      insertText: (context, event: { text: string }) => ({
        ...context,
        text: context.text + event.text,
      }),
    },
  })
);

editorStore.trigger.undo();
editorStore.trigger.redo();

详细模式:请参阅 references/store-patterns.md


XState + TanStack Query 集成

使用 QueryObserver 订阅查询

使用 fromCallback 和 QueryObserver 实现响应式订阅:

import { fromCallback } from "xstate";
import { QueryObserver, type QueryClient } from "@tanstack/react-query";

const subscribeToQuery = fromCallback(({ input, sendBack }) => {
  const { queryClient, entityId } = input as {
    queryClient: QueryClient;
    entityId: string;
  };

  const observer = new QueryObserver(queryClient, {
    queryKey: ["data", entityId],
    queryFn: async () => { /* fetch logic */ },
  });

  const unsubscribe = observer.subscribe((result) => {
    if (result.data) {
      sendBack({ type: "REMOTE_UPDATE", data: result.data });
    }
  });

  // Emit current result immediately
  const currentResult = observer.getCurrentResult();
  if (currentResult.data) {
    sendBack({ type: "REMOTE_UPDATE", data: currentResult.data });
  }

  return () => unsubscribe();
});
使用 useMutation 执行变更

将 useMutation 钩子封装为 fromPromise Actor,并通过 .provide() 提供:

const machine = useMemo(
  () =>
    createDataMachine().provide({
      actors: {
        saveData: fromPromise(async ({ input }) => {
          return await saveMutation.mutateAsync(input.data);
        }),
      },
    }),
  [saveMutation]
);

详细模式:请参阅 references/query-patterns.md


基本模式(适用于所有工具)

  1. 所有状态机都使用 setup() - 提供更出色的类型推断
  2. 事件类型使用枚举 - 遵循 UPPER_SNAKE_CASE 约定
  3. 在 setup() 外定义操作 - 使用 setup().assign() 等类型绑定辅助函数
  4. 使用 enqueueActions() 执行顺序操作 - 将相关操作组合在一起
  5. React 集成使用 .provide() - 使用外部方法覆盖操作
  6. 从 input 初始化上下文 - 动态初始化时使用函数形式
  7. 存储中始终展开 ...context - 保留其他属性
  8. 清理订阅 - 从回调 Actor 返回清理函数

何时使用各工具

以下情况使用 XState 状态机:
  • 需要具有特定转换的有限状态
  • 需要守卫和条件转换逻辑
  • 需要调用 Actor 进行异步编排
  • 需要层级状态或并行状态
  • 构建复杂的多步骤工作流
使用 @xstate/store 的场景:
  • 简单的全局或局部状态管理
  • 无需复杂流程的事件驱动状态更新
  • 需要撤销/重做功能
  • 希望使用轻量级替代方案,并可在之后升级到 XState
使用 XState + TanStack Query 的场景:
  • 需要协调数据获取与 UI 状态
  • 构建具有缓存同步功能的实时特性
  • 管理包含服务端状态的复杂异步工作流

应避免的常见问题

所有 XState 工具
  • 不要使用泛型参数;请改用 setup()
  • 不要在 setup() 中定义操作;请在外部使用类型绑定辅助函数定义操作
  • 不要使用字符串字面量作为事件类型;请改用枚举
  • 不要直接修改上下文;始终使用 assign() 或返回新对象
  • 不要让守卫函数具有副作用
  • 不要使用已弃用的 v4 API(interpret()、cond 等)
  • 不要忘记对 Actor 调用 .start()
@xstate/store 特有事项
  • 返回值中切勿忘记展开 ...context
  • 切勿订阅后不清理
  • 切勿将 store 用于复杂状态机(请迁移到 XState)
TanStack Query 集成
  • 不要在回调 Actor 外创建 QueryObserver
  • 不要在状态机操作中更新查询缓存
  • 不要将变更钩子直接传给 Actor(请用 fromPromise 包装)
  • 不要从闭包访问 QueryClient;请通过上下文传递

验证清单

完成任何涉及 XState 生态的任务前:

状态机
  • 状态机使用 setup() API 确保类型安全
  • 使用枚举定义事件类型
  • 在 setup() 外使用类型绑定辅助函数定义操作
  • 使用函数形式从 input 初始化上下文
  • 守卫是没有副作用的纯函数
  • React 组件使用 useMachine() 或 useActorRef()
@xstate/store
  • 始终在转换返回值中展开 ...context
  • 使用 trigger API 类型安全地发送事件
  • 创建选择器以优化订阅
  • 在 React 组件中清理订阅
TanStack Query 集成
  • 使用 fromCallback 包装 QueryObserver 订阅
  • 使用 fromPromise 包装变更,并通过 .provide() 提供
  • 在变更回调中更新查询缓存,而非在状态机操作中更新
  • 从回调 Actor 返回清理函数
  • 通过状态机的 input/上下文传递 QueryClient
始终执行
  • 运行类型检查(pnpm run typecheck)
  • 运行测试(pnpm run test)

详细参考文档

如需全面了解使用模式、示例和深入指南,请查阅以下参考文件:

  • references/machine-patterns.md - 完整的 XState 状态机使用模式,包括层级状态、并行状态、Actor、测试以及从 v4 迁移
  • references/store-patterns.md - 完整的 @xstate/store 使用模式,包括选择器、原子、效果、撤销/重做,以及何时迁移到状态机
  • references/query-patterns.md - 完整的 TanStack Query 集成模式,包括 QueryObserver、变更和完整的状态机示例

评论

支持 Markdown