使用 Xstate 和 ReactJS 的纯 UI

2025-06-08

使用 Xstate 和 ReactJS 的纯 UI

我们将介绍状态机是什么,以及类似“状态机 2.0”的状态图如何帮助您构建更强大的应用程序。

我们将使用xstate,它是一个statechart库和 ReactJS。但你实际上可以reactJS用任何其他框架来替换它。

总体目标是通过让 UI 成为状态函数来减少开发 UI 时的认知负荷。

当前状态 用户界面
列表 显示列表
列表加载 显示特定列表加载图像
无结果 显示无结果消息

这篇文章的代码可以在以下位置找到:

GitHub 徽标 criso /票务机器人

使用 Xstate 和 ReactJs 构建的聊天机器人示例

流动

我们介绍了状态机是什么,以及类似“状态机 2.0”的状态图如何帮助您构建更强大的应用程序。

它使用 Xstate(statecharts)和 reactJS 来构建聊天机器人流程🔥

该项目由Create React App引导

可用脚本

在项目目录中,您可以运行:

npm start

以开发模式运行应用程序。
打开http://localhost:3000在浏览器中查看。

如果您进行编辑,页面将重新加载。
您还将在控制台中看到任何 Lint 错误。




状态机?

我一直觉得状态机这个词有点奇怪。
一开始把它理解成:

该函数仅根据给定的输入执行与应用程序当前状态相关的操作。

const currentState = "isLoading";
function machine(input) {
  if (currentState === "isLoading") {
    // *only* do things related to `isLoading` state with `input`
  }

  if (currentState === "isError") {
    // *only* do things related to `isError` state with `input`
  }
}
Enter fullscreen mode Exit fullscreen mode

这是一个熟悉的状态机:

// currentState is `idle`

fetch() // currentState is `fetching`
.then(
  (successResults) => {
    //  currentState is 'success'
    // stateful data is 'successResults'
  }
  (errorMsg) => {
    // currentState is 'error'
    // stateful data is 'errorMsg'
  }
);
Enter fullscreen mode Exit fullscreen mode

因为一次currentState只能做一件事,所以你不会遇到这些检查:

 // NOPE, NOPE, NOPE
if (isLoading && !isError) // ...
if (!isLoading && isError) // ...
if (isLoading && isError) // ...
Enter fullscreen mode Exit fullscreen mode

一个有效的复杂系统总是从一个简单的有效系统发展而来。——约翰·加尔

两种类型的国家

有两种类型的状态:

  1. 应用的当前状态。这些回答了以下问题:
  • “正在加载吗?”
  • “有错误吗?”
  • “我们正在获取用户数据吗?”

这里的答案将决定使用哪个组件:

if (currentState === 'error') {
  return <Error />;
}
Enter fullscreen mode Exit fullscreen mode
  1. 有状态数据。这context在 中被称为xState。它们可以回答以下问题:
  • “错误信息是什么?”
  • “API请求的结果是什么?”
  • “当前选择了哪个过滤器/选项?”

这里的答案将决定组件具有哪些道具:

if (currentState === 'error') {
  return <Error msg={context.errorMsg}>
}
Enter fullscreen mode Exit fullscreen mode

告诉我我们处于哪种状态,我就能告诉你 UI 是什么样子的

UI 应该是一个状态函数。
这与 UI 是我们当前拥有的数据函数不同。

👍 状态函数:

if (currentState === list.noResults) {
  return "No Results found";
}

if (currentState === list.isError) {
  return "Oops!";
}
Enter fullscreen mode Exit fullscreen mode

对比

👎 我们目前拥有的数据:

if (list.length === 0) {
  // the list is empty, so we probably don't have any results"
  return "No Results found";
}

if (list.errMsg) {
  // list.err is not empty, show an error message #yolo
  return "Oops";
}
Enter fullscreen mode Exit fullscreen mode
☝️这是一个重要的区别。☝️

这里的对话从以下转变:

如果结果为零该怎么办?
记住:
结果为零可能是因为错误,也可能是因为我们还未获取任何数据,或者是因为确实没有任何结果。每种情况都代表着不同的状态。

到:

“当我们处于errorinitial或状态时,UI 是什么样的noResults?”

您现在正在构建 UI 来解释每个状态。

标题会变吗?
图标会变吗?
某些功能会失效吗?
是否应该设置一个“重试”按钮?

状态图配置

状态图是一种可以包含其他状态机的状态机...甚至更多!

所有这些的基础是状态图的配置。

您声明:

  • 可能存在的状态loading, error, noResults, listing, details, etc..
  • 每个actions/events州内可能发生的情况只有当我们处于该州时才会发生action/TRY_AGAINlisting.error
  • conditionals/guards在进入其他状态之前需要传递的,例如:只有当我们收到成功响应时,我们才会进入该状态noResults,并且total === 0

配置一个状态机是非常酷的,从中你可以理解绝大多数的 UI 逻辑。

在查看解释之前,请先尝试理解下面的配置:

// guards.js - conditional functions used to determine what the next step in the flow is
const guards = {
  shouldCreateNewTicket: (ctx, { data }) => data.value === "new_ticket",
  shouldFindTicket: (ctx, { data }) => data.value === "find_ticket"
};

// actions.js - functions that perform an action like updating the stateful data in the app
const actions = {
  askIntroQuestion: ctx => {
    return {
      ...ctx,
      chat: ["How may I help you?"]
    };
  }
};

// constants/state.js constants to represent the current state of the app
const intro = "@state/INTRO";
const question = "@state/QUESTION";
const newTicket = "@state/NEW_TICKET";
const findTicket = "@state/FIND_TICKET";

// constants/actions.js: constants to represent actions to be taken
const ANSWER = "@state/ANSWER";

const config = Machine({
  initial: intro,
  states: {
    [intro]: {
      initial: question,
      on: {
        [ANSWER]: [
          {
            cond: "shouldCreateNewTicket",
            actions: "updateCtxWithAnswer",
            target: newTicket
          },
          {
            cond: "shouldFindTicket",
            actions: "updateCtxWithAnswer",
            target: findTicket
          }
        ]
      },
      states: {
        [question]: { onEntry: "askIntroQuestion" }
      }
    },
    [newTicket]: {},
    [findTicket]: {}
  }
}).withConfig({
  actions,
  guards
});
Enter fullscreen mode Exit fullscreen mode

图像

上面的代码片段如下:

  • 初始状态intro来自states.intro
    • 内部的初始状态intro问题
    • onEntry我们将intro.question采取行动askIntroQuestion
    • 这里什么也没发生...UI 处于空闲状态...现在我们等待
    • 关于某项ANSWER活动:
      • 如果shouldCreateNewTicket
      • updateCtxWithAnswer
      • 前往newTicket
      • 如果shouldFindTicket
      • updateCtxWithAnswer
      • 前往findTicket

可以在https://statecharts.github.io/xstate-viz/上进行可视化

图像

哟!这个可视化效果是根据实际代码构建的!

我❤️这个!

这些不是代码注释,也不是spec-32.pdf8 个月未更新的共享硬盘。

想象一下,这在多大程度上有助于推动有关产品流程的对话,以及它如何使利益相关者就应用程序的每个状态达成一致。

可以清楚地知道是否存在一个error国家,

或者是否应该有一个noResults与一个error国家

好的...让我们构建一个聊天机器人流程

图像

这是规格和流程...我知道很无聊...但请继续听我说。

规格:

作为用户,我希望能够:

  1. 创建新票据来订购商品
  2. 查找现有票证
  3. 如果适用的话,应该有loading状态和error状态

Create new ticket

  • 订购商品时:
    • 如果我们没有该商品库存:
    • 显示警告信息
    • 显示商品选项,缺货商品显示为灰色
    • 用户应该能够再次从选项中进行选择
    • 如果我们有库存:
    • 显示成功信息
    • 如果出现错误
    • 显示错误信息

Find ticket

  • 如果发现:

    • 显示所订购的内容
    • 询问用户是否愿意向该订单发送“ping”
  • 如果没有找到:

    • 显示警告消息
    • 询问用户是否愿意创建新的工单

以下是一些机器配置:

const flowMachine = Machine({
  initial: intro,
  states: {
    [intro]: {
      initial: question,
      on: {
        [ANSWER]: [
          {
            target: newTicket,
            cond: "shouldCreateNewTicket",
            actions: "updateCtxWithAnswer"
          },
          {
            target: findTicket,
            cond: "shouldFindTicket",
            actions: "updateCtxWithAnswer"
          }
        ]
      },
      states: {
        [question]: { onEntry: "askIntroQuestion" }
      }
    },

    [findTicket]: {
      initial: question,
      on: {
        [ANSWER]: { target: `.${pending}`, actions: 'updateCtxWithAnswer' }
      },
      states: {
        [question]: { onEntry: 'askFindTicket' },
        [error]: {},
        [noResults]: {},
        [pending]: {
          invoke: {
            src: 'getTicket',
            onDone: [
              {
                target: done,
                actions: 'updateCtxWithResults',
                cond: 'foundTicket'
              },
              { target: noResults }
            ],
            onError: error
          }
        },
        [done]: { type: 'final' }
      },
      onDone: pingTicket
  }
});
Enter fullscreen mode Exit fullscreen mode
  • findTicket
  • 一旦用户回答了问题,我们就会进入调用pending的状态promisegetTicket
  • 如果出现错误:
    • 我们搬到error
  • 别的
    • 如果foundTicket是真的,我们就转到done状态
    • 如果foundTicket为假,我们转到noResults状态

这是按状态渲染组件的一种方法

根据当前状态渲染组件非常棒。


以下是根据currentState应用情况选择渲染组件或传递不同 props 的众多方法之一。
再次强调:
currentState此处指的是应用状态,“isLoading、error 等”
currentState.context指的是当前状态数据。

/**
 * Array of
 * [].<StateName, function>
 *
 * NOTE: specificity matters here so a more specific state
 * should be first in the list. e.g:
 * 'findTicket.noResults'
 * 'findTicket'
 *
 * On state 'findTicket.foo', 'findTicket' will be matched
 */
const stateRenderers = [
  [newTicket, ({ onSelect, currentState }) =>
    <Choices
      options={currentState.context.options}
      onSelect={onSelect} />
  ],

  [`${findTicket}.${noResults}`, () =>
    <Msg>Sorry, we can't find your ticket</Msg>],

  [`${findTicket}.${error}`, () => <Msg>Oops, we ran into an error!</Msg>],

  [findTicket, ({ onSelect }) => <FindTicketForm onSelect={onSelect} />]
];

// components/Choices.jsx
const Choices = ({ currentState, ...props}) => (
  // based on current state, get a function from `stateRenders`
  // and render it with the props we have
  const [stateName, renderState] =
      stateRenderers.find(([key]) => currentState.matches(key));

  return renderState(props);
)
Enter fullscreen mode Exit fullscreen mode

这是...

!图像

这是根据当前应用程序状态显示组件的不同设置

这里需要注意的是, 一次currentState只能做一件事,所以你不需要在这里进行布尔检查

isLoadingerror

<ChatBody data-testid="ChatBody">
  // display any chat info that exists in context
  {currentState.context.chat.map(({ question, answer }) => (
    <React.Fragment key={`${question}.${answer}`}>
      <ChatMsgQuestion>{question}</ChatMsgQuestion>
      {answer && <ChatMsgAnswer>{answer}</ChatMsgAnswer>}
    </React.Fragment>
  ))}

  // display message based on the current state that we're in
  // NOTE: only one of this is possible at a time
  {currentState.matches(pending) && <ChatMsgLoading />}
  {currentState.matches(error) && <ChatMsgError />}

  {currentState.matches(noResults) && (
    <ChatMsgWarning>{getNoResultsMsg(currentState)}</ChatMsgWarning>
  )}

  {currentState.matches(itemOrdered) && (
    <ChatMsgSuccess>{getSuccessMsg(currentState)}</ChatMsgSuccess>
  )}
</ChatBody>
Enter fullscreen mode Exit fullscreen mode

总结

好吧……希望你已经读到这里了。
查看代码了解更多。

我认为这很好地建立在已经发挥作用的模式之上,redux例如消息传递、一个流向、数据管理与组件的分离。

我发现使用这种模式可以非常轻松地适应需求变化。

事情是这样的:

  1. 规格变更
  2. 首先调整状态机配置
  3. 在 UI 中反映新状态

好的,我们只需要进入这个新状态。一旦我们进入这个状态,我们只需要让 UI 反映出这个特定状态应该是什么样子。

观点

  1. 这能取代 Redux 吗?是的。但是 Redux 的模式仍然适用。
    • 有一个可以根据事件减少数据的地方
    • 数据单向流动
    • 单独的 API
  2. 那么螺旋桨钻探怎么样?
    • 我认为这个问题被夸大了。
    • 你可以更好地分解你的组件或者使用 react.context

推荐阅读

https://xstate.js.org
https://statecharts.github.io

鏂囩珷鏉ユ簮锛�https://dev.to/cris_o/pure-ui-using-xstate-and-reactjs-5em7
PREV
软件开发中的前端、后端和全栈的解释。通用名称。Web 开发 vs Web 应用程序开发。构建 Web 应用程序。
NEXT
成为一名成功的开发顾问所需的 14 项技能