# 与 React 一起使用
¥Usage with React
这些 XState v4 文档不再维护
XState v5 现已推出!阅读有关 XState v5 的更多信息 (opens new window) 和 查看 XState v5 文档 (opens new window)。
¥XState v5 is out now! Read more about XState v5 (opens new window) and check out the XState v5 docs (opens new window).
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 的 useMachine
和 useInterpret
钩子可以与其他钩子一起使用。最常见的有两种模式:
¥XState's useMachine
and useInterpret
hooks can be used alongside others. Two patterns are most common:
# 命名动作/服务/守卫
¥Named actions/services/guards
让我们想象一下,当你导航到某个状态时,你想要离开该页面并通过 react-router
或 next
前往其他地方。现在,我们将该操作声明为 '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.
machine
是 interpreted,其service
实例放置在组件实例上。¥The
machine
is interpreted and itsservice
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 viathis.service.start()
.当组件卸载时,
service
通过this.service.stop()
停止。¥When the component will unmount, the
service
is stopped viathis.service.stop()
.事件通过
this.service.send(event)
发送到service
。¥Events are sent to the
service
viathis.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>
);
}
}
← 任务 4:计时器 与 Vue 一起使用 →