# 与 React 一起使用

¥Usage with React

XState 可以与 React 一起使用来:

¥XState can be used with React to:

  • 协调本地状态

    ¥Coordinate local state

  • 高效管理全局状态

    ¥Manage global state performantly

  • 使用其他钩子的数据

    ¥Consume data from other hooks

Stately (opens new window),我们喜欢这个组合。这是我们创建内部应用的首选堆栈。

¥At Stately (opens new window), we love this combo. It's our go-to stack for creating internal applications.

要寻求帮助,请查看 我们 Discord 社区中的 #react-help 通道 (opens new window)

¥To ask for help, check out the #react-help channel in our Discord community (opens new window).

# 本地状态

¥Local state

使用 React 钩子 (opens new window) 是在组件中使用状态机的最简单方法。你可以使用官方的 @xstate/react (opens new window) 为你提供开箱即用的有用钩子,例如 useMachine

¥Using React hooks (opens new window) are the easiest way to use state machines in your components. You can use the official @xstate/react (opens new window) to give you useful hooks out of the box, such as useMachine.

import { useMachine } from '@xstate/react';
import { toggleMachine } from '../path/to/toggleMachine';

function Toggle() {
  const [current, send] = useMachine(toggleMachine);

  return (
    <button onClick={() => send('TOGGLE')}>
      {current.matches('inactive') ? 'Off' : 'On'}
    </button>
  );
}

# 全局 State/React 上下文

¥Global State/React Context

我们推荐使用 XState 和 React 管理全局状态的方法是使用 React 上下文 (opens new window)

¥Our recommended approach for managing global state with XState and React is to use React Context (opens new window).

'context' 有两个版本:XState 的 context 和 React 的上下文。有点混乱!

¥There are two versions of 'context': XState's context and React's context. It's a little confusing!

# 上下文提供者

¥Context Provider

React 上下文可能是一个棘手的工具 - 如果你传递的值经常更改,则可能会导致树中的所有内容都重新渲染。这意味着我们需要传递尽可能少变化的值。

¥React context can be a tricky tool to work with - if you pass in values which change too often, it can result in re-renders all the way down the tree. That means we need to pass in values which change as little as possible.

幸运的是,XState 为我们提供了一种一流的方法来做到这一点:useInterpret

¥Luckily, XState gives us a first-class way to do that: useInterpret.

import React, { createContext } from 'react';
import { useInterpret } from '@xstate/react';
import { authMachine } from './authMachine';

export const GlobalStateContext = createContext({});

export const GlobalStateProvider = (props) => {
  const authService = useInterpret(authMachine);

  return (
    <GlobalStateContext.Provider value={{ authService }}>
      {props.children}
    </GlobalStateContext.Provider>
  );
};

使用 useInterpret 返回一个服务,该服务是对可以订阅的正在运行的机器的静态引用。这个值永远不会改变,所以我们不需要担心浪费的重新渲染。

¥Using useInterpret returns a service, which is a static reference to the running machine which can be subscribed to. This value never changes, so we don't need to worry about wasted re-renders.

对于 Typescript,你可以将上下文创建为 createContext({ authService: {} as InterpreterFrom<typeof authMachine> }); 以确保强类型。

¥For Typescript, you can create the context as createContext({ authService: {} as InterpreterFrom<typeof authMachine> }); to ensure strong typings.

# 利用上下文

¥Utilizing context

在树的更下方,你可以像这样订阅服务:

¥Further down the tree, you can subscribe to the service like this:

import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useActor } from '@xstate/react';

export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);
  const [state] = useActor(globalServices.authService);

  return state.matches('loggedIn') ? 'Logged In' : 'Logged Out';
};

useActor 钩子会监听服务何时发生更改,并更新状态值。

¥The useActor hook listens for whenever the service changes, and updates the state value.

# 提高性能

¥Improving Performance

上面的实现有问题 - 这将针对服务的任何更改更新组件。像 Redux (opens new window) 这样的工具使用 selectors (opens new window) 来导出状态。选择器是限制状态的哪些部分可以导致组件重新渲染的函数。

¥There's an issue with the implementation above - this will update the component for any change to the service. Tools like Redux (opens new window) use selectors (opens new window) for deriving state. Selectors are functions which restrict which parts of the state can result in components re-rendering.

幸运的是,XState 公开了 useSelector 钩子。

¥Fortunately, XState exposes the useSelector hook.

import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useSelector } from '@xstate/react';

const loggedInSelector = (state) => {
  return state.matches('loggedIn');
};

export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);
  const isLoggedIn = useSelector(globalServices.authService, loggedInSelector);

  return isLoggedIn ? 'Logged In' : 'Logged Out';
};

如果需要在消费服务的组件中发送事件,可以直接使用 service.send(...) 方法:

¥If you need to send an event in the component that consumes a service, you can use the service.send(...) method directly:

import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useSelector } from '@xstate/react';

const loggedInSelector = (state) => {
  return state.matches('loggedIn');
};

export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);
  const isLoggedIn = useSelector(globalServices.authService, loggedInSelector);
  // Get `send()` method from a service
  const { send } = globalServices.authService;

  return (
    <>
      {isLoggedIn && (
        <button type="button" onClick={() => send('LOG_OUT')}>
          Logout
        </button>
      )}
    </>
  );
};

仅当 state.matches('loggedIn') 返回不同的值时,该组件才会重新渲染。当你想要优化性能时,这是我们在 useActor 上推荐的方法。

¥This component will only re-render when state.matches('loggedIn') returns a different value. This is our recommended approach over useActor for when you want to optimise performance.

# 调度事件

¥Dispatching events

要将事件分派到全局存储,你可以直接调用服务的 send 函数。

¥For dispatching events to the global store, you can call a service's send function directly.

import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';

export const SomeComponent = (props) => {
  const globalServices = useContext(GlobalStateContext);

  return (
    <button
      onClick={() => globalServices.authService.send({ type: 'LOG_OUT' })}
    >
      Log Out
    </button>
  );
};

请注意,你不需要为此调用 useActor,它可以在上下文中直接使用。

¥Note that you don't need to call useActor for this, it's available right on the context.

# 其他钩子

¥Other hooks

XState 的 useMachineuseInterpret 钩子可以与其他钩子一起使用。最常见的有两种模式:

¥XState's useMachine and useInterpret hooks can be used alongside others. Two patterns are most common:

# 命名动作/服务/守卫

¥Named actions/services/guards

让我们想象一下,当你导航到某个状态时,你想要离开该页面并通过 react-routernext 前往其他地方。现在,我们将该操作声明为 'named' 操作 - 我们现在命名它并稍后声明它。

¥Let's imagine that when you navigate to a certain state, you want to leave the page and go somewhere else, via react-router or next. For now, we'll declare that action as a 'named' action - where we name it now and declare it later.

import { createMachine } from 'xstate';

export const machine = createMachine({
  initial: 'toggledOff',
  states: {
    toggledOff: {
      on: {
        TOGGLE: 'toggledOn'
      }
    },
    toggledOn: {
      entry: ['goToOtherPage']
    }
  }
});

在你的组件内,你现在可以实现指定的操作。我已经从 react-router 添加了 useHistory 作为示例,但你可以想象这适用于任何基于 hook 或 prop 的路由。

¥Inside your component, you can now implement the named action. I've added useHistory from react-router as an example, but you can imagine this working with any hook or prop-based router.

import { machine } from './machine';
import { useMachine } from '@xstate/react';
import { useHistory } from 'react-router';

const Component = () => {
  const history = useHistory();

  const [state, send] = useMachine(machine, {
    actions: {
      goToOtherPage: () => {
        history.push('/other-page');
      }
    }
  });

  return null;
};

这也适用于服务、守卫和延误。

¥This also works for services, guards, and delays.

如果你使用此技术,你在 goToOtherPage 内使用的任何参考都将在每次渲染时保持最新。这意味着你无需担心过时的引用。

¥If you use this technique, any references you use inside goToOtherPage will be kept up to date each render. That means you don't need to worry about stale references.

# 与 useEffect 同步数据

¥Syncing data with useEffect

有时,你想将某些功能外包给另一个钩子。这对于 react-query (opens new window)swr (opens new window) 等数据获取钩子尤其常见。你不想在 XState 中重新构建所有数据获取功能。

¥Sometimes, you want to outsource some functionality to another hook. This is especially common with data fetching hooks such as react-query (opens new window) and swr (opens new window). You don't want to have to re-build all your data fetching functionality in XState.

管理此问题的最佳方法是通过 useEffect

¥The best way to manage this is via useEffect.

const Component = () => {
  const { data, error } = useSWR('/api/user', fetcher);

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

  useEffect(() => {
    send({
      type: 'DATA_CHANGED',
      data,
      error
    });
  }, [data, error, send]);
};

每当 useSWR 的结果发生变化时,这将发送 DATA_CHANGED 事件,允许你像任何其他事件一样对其做出反应。例如,你可以:

¥This will send a DATA_CHANGED event whenever the result from useSWR changes, allowing you to react to it just like any other event. You could, for instance:

  • 当数据返回错误时进入 errored 状态

    ¥Move into an errored state when the data returns an error

  • 将数据保存到上下文

    ¥Save the data to context

# 类组件

¥Class components

如果你使用类组件,这里有一个不依赖钩子的示例实现。

¥If you're using class components, here's an example implementation that doesn't rely on hooks.

  • machineinterpreted,其 service 实例放置在组件实例上。

    ¥The machine is interpreted and its service instance is placed on the component instance.

  • 对于本地状态,this.state.current 将保存当前机器状态。你可以使用 .current 以外的属性名称。

    ¥For local state, this.state.current will hold the current machine state. You can use a property name other than .current.

  • 安装组件后,通过 this.service.start() 启动 service

    ¥When the component is mounted, the service is started via this.service.start().

  • 当组件卸载时,service 通过 this.service.stop() 停止。

    ¥When the component will unmount, the service is stopped via this.service.stop().

  • 事件通过 this.service.send(event) 发送到 service

    ¥Events are sent to the service via this.service.send(event).

import React from 'react';
import { interpret } from 'xstate';
import { toggleMachine } from '../path/to/toggleMachine';

class Toggle extends React.Component {
  state = {
    current: toggleMachine.initialState
  };

  service = interpret(toggleMachine).onTransition((current) =>
    this.setState({ current })
  );

  componentDidMount() {
    this.service.start();
  }

  componentWillUnmount() {
    this.service.stop();
  }

  render() {
    const { current } = this.state;
    const { send } = this.service;

    return (
      <button onClick={() => send('TOGGLE')}>
        {current.matches('inactive') ? 'Off' : 'On'}
      </button>
    );
  }
}