“只使用 Props”:React 和 XState 的指南

2025-06-09

“只使用 Props”:React 和 XState 的指南

XState 可能会让人感到不知所措。一旦你完成了KyleDavid 的课程并阅读了相关文档,你就能彻底理解这个 API。你会发现 XState 是管理复杂状态最强大的工具。

将 XState 与 React 集成时,挑战就来了。状态机应该放在 React 树的哪个位置?如何管理父状态机和子状态机?

只需使用道具

我想为 XState 和 React 提出一个架构,它优先考虑简单性、可读性和类型安全。它可以逐步采用,并为您提供探索更复杂解决方案的基础。我们已经在Yozobi 的生产环境中使用了它,并计划在未来的每个项目中都使用它。

它被称为只使用 props。它有一些简单的规则:

  1. 创建机器。不要太多。主要使用机器
  2. 让 React 处理树
  3. 尽可能保持本地状态

创建机器。不要太多。主要使用机器

在您的应用程序中集成状态机的最简单方法是使用useMachine



import { createMachine, interpret } from 'xstate';
import { useMachine } from '@xstate/react';

const machine = createMachine({
  initial: 'open',
  states: {
    open: {},
    closed: {},
  },
});

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

  return state.matches('open') ? 'Open' : 'Closed';
};


Enter fullscreen mode Exit fullscreen mode

请注意,这让 React 掌控了机器。机器与组件绑定,并遵循所有 React 数据向下流动的常规规则。换句话说,你可以将它想象成useStateuseReducer,但这是一个大大改进的版本

让 React 处理树

假设你有一个父组件和一个子组件。父组件需要将一些状态传递给子组件。有几种方法可以实现这一点。

通过 props 传递服务

第一种是将一个正在运行的服务传递给孩子,孩子可以订阅该服务:



import { useMachine, useService } from '@xstate/react';
import { createMachine, Interpreter } from 'xstate';

/**
 * Types for the machine declaration
 */
type MachineContext = {};
type MachineEvent = { type: 'TOGGLE' };

const machine = createMachine<MachineContext, MachineEvent>({});

const ParentComponent = () => {
  /**
   * We instantiate the service here...
   */
  const [state, send, service] = useMachine(machine);

  return <ChildComponent service={service} />;
};

interface ChildComponentProps {
  service: Interpreter<MachineContext, any, MachineEvent>;
}

const ChildComponent = (props: ChildComponentProps) => {
  /**
   * ...and receive it here
   */
  const [state, send] = useService(props.service);

  return (
    <button onClick={() => send('TOGGLE')}>
      {state.matches('open') ? 'Open' : 'Closed'}
    </button>
  );
};


Enter fullscreen mode Exit fullscreen mode

我不喜欢这种模式。对于不熟悉 XState 的人来说,很难理解“服务”的含义。我们无法通过读取类型来获得清晰的理解,尤其是在Interpreter使用多个泛型的情况下,这尤其难看。

这台机器似乎渗透到了多个组件中。它的服务似乎在 React 树之外拥有自己的生命。对于新手来说,这感觉像是误导。

只需传递道具

使用 props 可以更清晰地表达这一点:



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

/**
 * Types for the machine declaration
 */
type MachineContext = {};
type MachineEvent = { type: 'TOGGLE' };

const machine = createMachine<MachineContext, MachineEvent>({});

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

  return (
    <ChildComponent
      isOpen={state.matches('open')}
      toggle={() => send('TOGGLE')}
    />
  );
};

/**
 * Note that the props declarations are
 * much more specific
 */
interface ChildComponentProps {
  isOpen: boolean;
  toggle: () => void;
}

const ChildComponent = (props: ChildComponentProps) => {
  return (
    <button onClick={() => props.toggle()}>
      {props.isOpen ? 'Open' : 'Closed'}
    </button>
  );
};


Enter fullscreen mode Exit fullscreen mode

好多了。我们在清晰度方面得到了一些改进ChildComponent——类型更容易阅读了。我们可以完全放弃使用InterpreteranduseService了。

不过,最好的改进在于ParentComponent。在上一个示例中,机器通过传递服务来跨多个组件。在本例中,它的作用域限定在组件内,并且 props 源自其状态。对于不习惯 XState 的人来说,这更容易理解。

尽可能保持本地状态

与那些需要全局存储的工具不同,XState 对状态的保存位置没有任何要求。如果你有一个状态位于应用根目录附近,可以使用 React Context 使其全局可用:



import React, { createContext } from 'react';
import { useMachine } from '@xstate/react';
import { createMachine } from 'xstate';

const globalMachine = createMachine({});

interface GlobalContextType {
  isOpen: boolean;
  toggle: () => void;
}

export const GlobalContext = createContext<GlobalContextType>();

const Provider: React.FC = ({ children }) => {
  const [state, send] = useMachine(globalMachine);

  return (
    <GlobalContext.Provider
      value={{ isOpen: state.matches('open'), toggle: () => send('TOGGLE') }}
    >
      {children}
    </GlobalContext.Provider>
  );
};


Enter fullscreen mode Exit fullscreen mode

正如上面一样,我们不是将服务,而是道具传递到上下文中。

如果您的状态片段需要位于树的较低位置,则请遵循通常的规则,将状态提升到所需的位置。

如果你觉得这很熟悉,那你是对的。你正在做着和以前一样的决定:在哪里存储状态以及如何传递它。

示例和挑战

同步父母与孩子

有时,您需要使用一台主机器一台子机器。假设您需要子机器关注主机器中某个 prop 的变化——例如同步某些数据。您可以这样做:



const machine = createMachine({
  initial: 'open',
  context: {
    numberToStore: 0,
  },
  on: {
    /**
     * When REPORT_NEW_NUMBER occurs, sync
     * the new number to context
     */
    REPORT_NEW_NUMBER: {
      actions: [
        assign((context, event) => {
          return {
            numberToStore: event.newNumber,
          };
        }),
      ],
    },
  },
});

interface ChildComponentProps {
  someNumber: number;
}

const ChildComponent = (props: ChildComponentProps) => {
  const [state, send] = useMachine(machine);

  useEffect(() => {
    send({
      type: 'REPORT_NEW_NUMBER',
      newNumber: props.someNumber,
    });
  }, [props.someNumber]);
};


Enter fullscreen mode Exit fullscreen mode

这也可以用于同步来自其他来源的数据,例如查询钩子:



const ChildComponent = () => {
const [result] = useSomeDataHook(() => fetchNumber());

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

useEffect(() => {
send({
type: 'REPORT_NEW_NUMBER',
newNumber: result.data.someNumber,
});
}, [result.data.someNumber]);
};

Enter fullscreen mode Exit fullscreen mode




概括

在“仅使用 props”的方案中,XState 让 React 掌控全局。我们坚持 React 的惯用做法,传递 props,而不是服务。我们将机器的作用域限定在组件内。并且,我们将状态置于所需的级别,就像您习惯的那样。

这篇文章尚未完成。我相信关于 XState 与 React 集成的问题还会有很多。我计划以后再回来看这篇文章,提供更多示例和说明。感谢您抽出时间,期待看到您使用 XState 构建的作品。

鏂囩珷鏉ユ簮锛�https://dev.to/mattpocockuk/just-use-props-an-opinionated-guide-to-react-and-xstate-fc9
PREV
JavaScript 基础知识:数据结构和算法初学者指南
NEXT
如何使用 XState 和 React 管理全局状态