Introducing XState Store XState 商店简介
— JavaScript, TypeScript, State Management, XState — 4 min read
2024 年 8 月 1 日 — JavaScript、TypeScript、状态管理、XState — 阅读 4 分钟
- No translations available.
- Add translation 添加翻译
没有可用的翻译。
I'm super happy with the tech stack I'm currently using, especially around state management. Obviously, server state is managed by React Query. For forms, I use React Hook Form.
我对我目前使用的技术堆栈非常满意,尤其是在状态管理方面。显然,服务器状态是由 React Query 管理的。对于表单,我使用 React Hook Form。
What remains can very often be put into the url, which is really a joy with TanStack Router. If that's not a good fit, I use Zustand - my favourite client state manager to date.
剩下的内容通常可以放入 url 中,这对 TanStack Router 来说确实是一种乐趣。如果这不合适,我会使用 Zustand——迄今为止我最喜欢的客户端状态管理器。
This has been my recommended stack for quite some time (okay, the router is quite new, but the concept isn't), and I'm not known for easily switching state managers. Every once in a while, something new comes out, but in order for me to switch, it has to be quite a lot better than what I currently work with.
这已经是我推荐的堆栈很长一段时间了(好吧,路由器很新,但概念不是),而且我不擅长轻松切换状态管理器。每隔一段时间,就会出现一些新的东西,但为了让我进行转换,它必须比我目前使用的东西好很多。
But today might be that time.
但今天可能就是那个时候了。
xstate/store xstate/商店
When I first read about xstate/store, I was immediately intrigued by a couple of things. For one, it was made by David Khourshid, and whatever he builds usually overlaps conceptually with my thinking. And second, it felt like he totally nailed the API on xstate/store
. On the first glance, it looked like zustand
and redux-toolkit
had a child, combining the best of both libs.
当我第一次读到 xstate/store 时,我立即对一些事情很感兴趣。首先,它是由 David Khourshid 制作的,无论他构建什么,通常在概念上都与我的想法重叠。其次,感觉他完全掌握了 xstate/store
上的 API。乍一看, zustand
和 redux-toolkit
似乎有一个孩子,结合了两个库的优点。
Let's take a look at an example, and for easy comparison, I'll be using a similar example as the one from my working with zustand article:
让我们看一个示例,为了便于比较,我将使用与我的 zustand 文章中的示例相似的示例:
1import { createStore } from '@xstate/store'2import { useSelector } from '@xstate/store/react'3
4const store = createStore(5 // context6 {7 bears: 0,8 fish: 0,9 },10 // transitions11 {12 increasePopulation: (context, event: { by: number }) => ({13 bears: context.bears + event.by,14 }),15 eatFish: (context) => ({16 fish: context.fish - 1,17 }),18 removeAllBears: () => ({19 bears: 0,20 }),21 }22)23
24export const useBears = () =>25 useSelector(store, (state) => state.context.bears)26export const useFish = () =>27 useSelector(store, (state) => state.context.fish)
createStore
is the main function we need to use from xstate/store
, which is split into two parts: context and transitions. Conceptually, context
is the state of our store, while transitions
are similar to actions
.
createStore
是我们需要从 xstate/store
使用的主要函数,它分为两部分:上下文和转换。从概念上讲, context
是我们商店的状态,而 transitions
与 actions
类似。
One could say that this is only marginally different to zustand
, so what's intriguing about this? Well, to me, there are quite many things. Let's break it down:
有人可能会说这与 zustand
只是略有不同,那么这有什么有趣的呢?嗯,对我来说,有很多事情。让我们来分解一下:
TypeScript 打字稿
It will infer TypeScript types of the store
from the initial context. This is pretty great and something that was usually a lot more verbose with zustand
(There are some ways to make this better with the combine middleware).
它将从初始上下文推断 store
的 TypeScript 类型。这非常棒,而且 zustand
通常会更加冗长(有一些方法可以通过组合中间件来使其更好)。
Note that the above example is already in TypeScript, and the only thing we needed to manually type was the event
passed into our increasePopulation
transition. That's really how user-land TypeScript should be: The more it looks like plain JavaScript, the better.
请注意,上面的示例已经在 TypeScript 中,我们唯一需要手动输入的是将 event
传递到 increasePopulation
转换中。这确实是用户态 TypeScript 应该的样子:它看起来越像纯 JavaScript 就越好。
Transitions 过渡
The store has a natural split between state and actions, which is something that I recommend doing with zustand
as well. Except that in xstate/store
, transitions aren't part of the store state, so we don't have to select them to perform updates / exclude them when persisting the store somewhere etc.
商店在状态和操作之间有一个自然的划分,这也是我建议使用 zustand
做的事情。除了在 xstate/store
中,转换不是存储状态的一部分,因此我们不必选择它们来执行更新/在将存储保存在某处等时排除它们。
Event driven 事件驱动
Speaking of updates: if we don't select actions from the store - how do we trigger a transition? Quite simply with store.send
:
说到更新:如果我们不从商店中选择操作 - 我们如何触发转换?使用 store.send
非常简单:
1function App() {2 const bears = useBears()3
4 return (5 <div>6 Bears: {bears}7 <button8 onClick={() =>9 store.send({ type: 'increasePopulation', by: 10 })10 }11 >12 Increment13 </button>14 </div>15 )16}
It wouldn't be an xstate
like library if the store itself wasn't event driven. Again, this is something I've also recommended for doing with zustand
, because events are a lot more descriptive than setters and they make sure that the logic lives in the store, not in the UI that triggers the update.
如果商店本身不是事件驱动的,那么它就不会是一个类似 xstate
的库。同样,这也是我建议使用 zustand
执行的操作,因为事件比 setter 更具描述性,并且它们确保逻辑存在于存储中,而不是存在于触发更新的 UI 中。
So with store.send
, we are triggering a transition from one state to the next. It takes an object with type
, which is derived from the keys of the transition object we've defined on our store. And of course, it's totally type-safe.
因此,使用 store.send
,我们触发从一种状态到下一种状态的转换。它需要一个带有 type
的对象,该对象派生自我们在商店中定义的转换对象的键。当然,它是完全类型安全的。
This is where I've seen some similarities with redux toolkit, and dispatching events has always been my favourite part of the redux design.
这是我发现与 redux 工具包有一些相似之处的地方,并且调度事件一直是 redux 设计中我最喜欢的部分。
Selectors 选择器
Yes, zustand
is built on selectors as well, but notice how the created store isn't a hook itself - we have to pass it to useSelector
, which requires us to pass a selector function, too. That means we are less likely to subscribe to the complete store by accident, which is a common performance pitfall with zustand
. Additionally, we can also pass a comparison function as 3rd argument to useSelector
in case the default referential comparison isn't good enough.
是的, zustand
也是基于选择器构建的,但请注意创建的存储本身并不是一个钩子 - 我们必须将其传递给 useSelector
,这也要求我们传递一个选择器函数。这意味着我们不太可能意外订阅整个商店,这是 zustand
的常见性能陷阱。此外,我们还可以将比较函数作为第三个参数传递给 useSelector
,以防默认的引用比较不够好。
Framework agnostic 框架不可知论
Maybe you've seen it - creatStore
is imported from @xstate/store
while useSelector
is imported from @xstate/store/react
. That's because the store itself knows nothing about React, and the React adapter is literally just a wrapper around store.subscribe
put into useSyncExternalStore
.
也许您已经看到了 - creatStore
是从 @xstate/store
导入的,而 useSelector
是从 @xstate/store/react
导入的。这是因为商店本身对 React 一无所知,而 React 适配器实际上只是将 store.subscribe
放入 useSyncExternalStore
的包装器。
If this sounds familiar, maybe that's because TanStack Query has the same approach, so maybe we'll see different framework adapters for xstate/store
in the future, too.
如果这听起来很熟悉,也许是因为 TanStack Query 具有相同的方法,所以也许我们将来也会看到 xstate/store
的不同框架适配器。
Upgrade to state machines
升级到状态机
State machines have the reputation of being a complex tool to adopt, which is why a lot of people shy away from them. And I think it's true that they are likely "overkill" for most state that gets managed in web applications.
状态机被认为是一种使用起来很复杂的工具,这就是为什么很多人回避它们的原因。我认为,对于大多数在 Web 应用程序中管理的状态来说,它们确实可能“杀伤力过大”。
However, state usually evolves over time, getting more complex as requirements are added. I've seen lots of code in useReducer
or an external zustand
store where I thought: This should obviously be a state machine - why isn't this one?
然而,状态通常会随着时间的推移而演变,随着需求的增加而变得更加复杂。我在 useReducer
或外部 zustand
商店中看到了很多代码,我想:这显然应该是一个状态机 - 为什么不是这个呢?
The answer is usually that by the time we realize that it should be a state machine, it's already so complex that creating one out of it is not an easy thing to do anymore.
答案通常是,当我们意识到它应该是一个状态机时,它已经非常复杂,以至于用它创建一个状态机不再是一件容易的事情。
And that's again where xstate/store
shines because it offers a simple upgrade path to convert a store into a state machine. It might not be something you think you need, but it's exactly the thing that you're happy you have available for free if you need it.
这又是 xstate/store
的闪光点,因为它提供了一个简单的升级路径来将存储转换为状态机。它可能不是您认为需要的东西,但如果您需要它,您会很高兴可以免费获得它。
When my article working with zustand came out, it was very well received because it provides some opinionated guidance for working with a tool that mostly stays out of your way. It lets you structure and update your store the way you want to - total freedom that can also be a bit paralyzing.
当我使用 zustand 的文章发表时,它受到了热烈欢迎,因为它提供了一些关于如何使用大多数不妨碍您的工具的固执己见的指导。它可以让您按照您想要的方式构建和更新您的商店 - 完全自由,但也可能有点瘫痪。
xstate/store
feels to me like a more opinionated way of achieving the same thing. And the fact that the opinions overlap a lot (like really a lot) with how I would do things make it a very good choice for me.
xstate/store
对我来说感觉像是一种更固执己见的方式来实现同样的事情。事实上,这些观点与我做事的方式有很多重叠(真的很多),这对我来说是一个非常好的选择。
That's it for today. Feel free to reach out to me on twitter
if you have any questions, or just leave a comment below. ⬇️
今天就这样。如果您有任何疑问,请随时在 Twitter 上与我联系,或者在下面发表评论。 ⬇️