如何使用 XState 和 React 管理全局状态
本文已成为官方XState 文档的一部分!
许多 React 应用程序遵循由Redux推广的 Flux 架构。这种架构可以通过以下几个关键思想来体现:
- 它使用应用程序顶部的单个对象来存储所有应用程序状态,通常称为存储。
- 它提供了一个
dispatch
函数,可用于将消息发送到 store。Redux 调用了这些方法actions
,但我将直接调用它们events
——因为它们在 XState 中是已知的。 - 商店如何响应来自应用程序的这些消息以纯函数来表达 - 最常见的是使用reducer。
本文不会深入探讨 Flux 架构是否可行。David Khourshid 的文章《Redux 只是一种模式的一半》对此进行了详细的阐述。为了方便理解,我们假设您喜欢全局存储,并且希望在 XState 中复制它。
这样做有很多原因。在管理复杂的异步行为和建模难题方面,XState 是首屈一指的。在 Redux 应用中,管理这些功能通常需要中间件:redux-thunk、redux-loop或redux-saga。选择 XState 可以让你以一流的方式管理复杂性。
全球可用的商店
为了模拟 Redux 的全局可用 store,我们将使用 React context。React context 的使用可能比较棘手——如果传入的值变化过于频繁,可能会导致整个组件树都重新渲染。这意味着我们需要传入变化尽可能小的值。
幸运的是,XState 为我们提供了一流的方法来实现这一点。
import React, { createContext } from 'react';
import { useInterpret } from '@xstate/react';
import { authMachine } from './authMachine';
import { ActorRefFrom } from 'xstate';
interface GlobalStateContextType {
authService: ActorRefFrom<typeof authMachine>;
}
export const GlobalStateContext = createContext(
// Typed this way to avoid TS errors,
// looks odd I know
{} as GlobalStateContextType,
);
export const GlobalStateProvider = (props) => {
const authService = useInterpret(authMachine);
return (
<GlobalStateContext.Provider value={{ authService }}>
{props.children}
</GlobalStateContext.Provider>
);
};
使用useInterpret
会返回一个service
,它是正在运行的机器的静态引用,可以订阅它。这个值永远不会改变,所以我们不必担心浪费重新渲染的时间。
利用上下文
沿着树往下看,您可以像这样订阅服务:
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
钩子监听服务的每次变化并更新其state
值。
提高性能
上面的实现存在一个问题——服务的任何变更都会更新组件。Redux 提供了使用选择器获取状态的工具——选择器是用来限制哪些状态部分会导致组件重新渲染的函数。
幸运的是,XState 也提供了该功能。
import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useSelector } from '@xstate/react';
const selector = (state) => {
return state.matches('loggedIn');
};
export const SomeComponent = (props) => {
const globalServices = useContext(GlobalStateContext);
const isLoggedIn = useSelector(globalServices.authService, selector);
return isLoggedIn ? 'Logged In' : 'Logged Out';
};
现在,此组件仅在返回不同的值时才会重新渲染。如果您想要优化性能,state.matches('loggedIn')
我推荐这种方法。useActor
调度事件
要将事件分派到全局存储,您可以直接调用服务的send
功能。
import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
export const SomeComponent = (props) => {
const globalServices = useContext(GlobalStateContext);
return (
<button onClick={() => globalServices.authService.send('LOG_OUT')}>
Log Out
</button>
);
};
请注意,您不需要useActor
为此调用,它可以直接在上下文中使用。
与 Flux 的偏差
眼尖的读者可能会发现,这个实现与 Flux略有不同。例如,它不是使用单个全局存储,而是同时运行多个机器:authService
、dataCacheService
和globalTimeoutService
。它们每个都有自己的send
属性,因此你调用的不是全局调度。
这些变化是可以解决的。可以send
在全局存储中创建一个合成函数,send
手动调用所有服务的函数。但就我个人而言,我更喜欢确切地知道我的消息会传递给哪些服务,这样可以避免将事件保留在全局命名空间中。
概括
XState 可以完美地用作 React 应用程序的全局存储。它使应用程序逻辑保持共置,将副作用视为“一等公民”,并提供良好的性能useSelector
。如果您热衷于 Flux 架构,但又觉得应用程序的逻辑有些失控,那么您应该选择这种方法。