状态机
什么是状态机
基本介绍
能不能通俗的讲解下什么是状态机? - 知乎 (zhihu.com)
状态机是一种编程思路。一种对对自然界某种事物(或数据)状态变化的抽象。
让你的程序开发维护,思路更加清晰方便。
它规定了一个实例(某种抽象),某一时刻只能有一种状态(属性)。一般用字符串表示。
规定了只能通过实例的方法,即执行某个动作(函数),之后才可以改变状态(属性)。
就这两个规定。
再说有限状态机。和状态变更回调。
有限状态机,指上诉的状态机,总状态数种类是有限个。
且规定变化到某种状态,需要验证当前状态是都合理。实现的话,实例化时候需要接收允许从某状态,变化到哪些状态。然后写一个验证,里面就一行 if 判断是否允许。(例如订单,代码层面,不允许从已付款变成未付款,允许从未付款变成已付款,直接改数据库的不算)
状态变更回调,指状态变更动作之前和之后执行的函数。
状态变更动作可能是异步完成,有时候需要知道开始和结束。例如调试时候。
状态的顺序和路径以不可能破坏特定顺序的方式定义和限制。以咖啡机为例,如果没有事先加热和清洁,就不可能在打开设备后直接煮咖啡。这种机器称为有限状态机。
http 请求就是一个典型的有限状态机。状态有 open pending ending error等 ,stateChange ,success 等就是它的状态变更回调。且如果当前是 error ,不能从 error 变成 pending 了。
只要你满足了这些特征。就是状态机。
怎么实现是写法上不同,甚至实例化也不需要只用 new 。如果是自己实现,不用刻意照搬他人代码。
不过,几乎所有语言,都有别人写好的有限状态机的实现库,因为本身也没用多少难度,只要知道语法基础,就能写出来。希望能帮到你。
✔ 状态: 特定时间点的某个对象信息
✔ 转换: 状态之间的转换过程
✔状态机: 多个状态和状态之间的转换组成状态机
✔通常用来描述对象与时间的关系。
▲什么是状态机
状态机是有限状态自动机的简称。有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automaton,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。
关于有限的解释:也就是被描述的事物的状态的数量是有限的,例如开关的状态只有“开”和“关”两个;灯的状态只有“亮”和“灭”等等。
▲特点
一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到另一个状态,不过也可能存在瞬时状态,而一旦任务完成,状态机就会立刻离开瞬时状态。每个状态根据不同的前置条件,会从当前状态流转至下一个状态。
▲作用
使用状态机来表达状态的流转,会使语义会更加清晰,会增强代码的可读性和可维护性。
▲适用场景
面对复杂的状态流转(一般是超过三个及以上的状态流转),那么还是比较建议用状态机来实现的。
视频介绍
业务新解 | 如何解决当下如此高复杂度的业务场景 | 状态机 | XState_哔哩哔哩_bilibili
好处
目前来说,无论是 to c 业务,还是 to b 业务,对于前端开发者的要求越来越高,各种绚丽的视觉效果,复杂的业务逻辑层出不穷。针对于业务逻辑而言,贯穿后端业务和前端交互都有一个关键点 —— 状态转换。
当然了,这种代码实现本身并不复杂,真正的难点在于如何快速的进行代码的修改。
在实际开发项目的过程中,ETC 原则,即 Easier To Change,易于变更是非常重要的。为什么解耦很好? 为什么单一职责很有用? 为什么好的命名很重要?因为这些设计原则让你的代码更容易发生变更。ETC 甚至可以说是其他原则的基石,可以说,我们现在所作的一切都是为了更容易变更!!特别是针对于初创公司,更是如此。
例如:项目初期,当前的网页有一个模态框,可以进行编辑,模态框上有两个按钮,保存与取消。这里就涉及到模态框的显隐状态以及权限管理。随着时间的推移,需求和业务发生了改变。当前列表无法展示该项目的所有内容,在模态框中我们不但需要编辑数据,同时需要展示数据。这时候我们还需要管理按钮之间的联动。仅仅这些就较为复杂,更不用说涉及多个业务实体以及多角色之间的细微控制。
重新审视自身代码,虽然之前我们做了大量努力利用各种设计原则,但是想要快速而安全的修改散落到各个函数中的状态修改,还是非常浪费心神的,而且还很容易出现“漏网之鱼”。
这时候,我们不仅仅需要依靠自身经验写好代码,同时也需要一些工具的辅助。
关于状态机的技术选型
关于状态机的技术选型,最后一个真心好! - 知乎 (zhihu.com)
今天跟大家分享一个关于“状态机”的话题。状态属性在我们的现实生活中无处不在。
比如电商场景会有一系列的订单状态(待支付、待发货、已发货、超时、关闭);员工提交请假申请会有申请状态(已申请、审核中、审核成功、审核拒绝、结束);差旅报销单会有单据审核状态(已提交、审核中、审核成功、退回、打款中、打款成功、打款失败、结束)等等。
上述场景有一个共同问题:根据不同触发条件执行不同处理动作最后落地不同的状态。示例代码如下:
1 | Integer status=0; |
那我们最容易能想到的自然是if-else方案。那if-else方案会有什么问题呢?
主要有以下几点:
- 复杂的业务流程,if.else代码几乎无法维护
- 随着业务的发展,业务过程也需要变更及扩展,但if.else代码段已经无法支持
- 没有可读性,变更风险特别大,可能会牵一发而动全身,线上事故层出不穷
- 其他业务逻辑可能也会跟if-else代码块耦合在一起,带来更多的问题
状态机的出现就是用来解决上述问题的。在复杂多状态流转情况下,通过状态机的引入,我们希望相关代码可读性、扩展性能比if-else方案更好!
各个状态机方案
▲枚举状态机
Java中的枚举是一个定义了一系列常量的特殊类(隐式继承自class java.lang.Enum)。枚举类型因为自身的线程安全性保障和高可读性特性,是简单状态机的首选。
▲状态模式实现的状态机
是什么
状态模式是编程领域特有的名词,是 23 种设计模式之一,属于行为模式的一种。
它允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
作用状态模式的设计意图主要是为了解决两个主要问题:
- 当一个对象的内部状态改变时,它应该改变它的行为。
- 应独立定义特定于状态的行为。也就是说,添加新状态不应影响现有状态的行为
▲开源实现
前端状态机
前端状态机:XState 首个中文文档上线了_王乐平的博客-CSDN博客_前端状态机
目前开源的有限状态机实现中比较知名的有:
xstate
:堪称状态机航空母舰,功能太强大了,也太复杂了,学习成本非常高。Javascript State Machine
:功能较弱,在实际试用过程中发现在进行异步切换时存在问题。jssm
:特点是引入自己的DSL语法来描述状态机,使用起来比较别扭。
State Machines in JavaScript with XState
full video: https://www.bilibili.com/video/BV1Bb4y177EH
captions: State Machines in JavaScript with XState | Frontend Masters | Frontend Masters
docs: Welcome to the Stately docs | Stately Docs
Introduction
The “Introduction” Lesson is part of the full, State Machines in JavaScript with XState, v2 course featured in this preview video. Here’s what you’d learn in this lesson:
David Khourshid introduces the course by providing a brief overview of the course material and walks through the course repository. A demonstration of the final state of the project is also provided in this segment.
Hello, everyone, my name is David Khourshid, and I’m really excited to be doing another workshop on state machines with XState in JavaScript. And we’re gonna be discussing a lot more than version one of the workshop. So if you’ve taken version one, and you’ve taken the React’s versions of the workshop, then we’re going to be building on top of that.
Still working in vanilla JavaScript, but we’re going to be going over a lot of things today. We’re gonna be going over software modeling, when to use state machines and when to use state charts, all the fundamental parts of state machines and state charts. They’re going to be useful in your everyday applications.
And we’re also gonna be talking a lot about the actor model and how you could use actors to represent the different parts of your application that are communicating with each other. So like I said, my name is David Khourshid, I work at Stately, which is a company that I founded.
And we’re creating a lot of tools for visualizing application logic, whether it’s with state machines, or state charts, or other types of diagrams. So a lot of what we do today is actually going to be represented in the tools that we’re building at Stately. I’m also on Twitter @DavidKPiano, so please feel free to follow me, reach out to me, DM me, whatever you want.
So before we get started, I wanna talk about how this workshop is going to be structured. So you should have received the link to the GitHub repo, it’s at https//github.com/davidkpiano/frontend-masters-xstate-v. The previous workshop is Frontend Masters XState Workshop. So if you do wanna go back and review that, then a lot of the material will be similar, but there’s also a lot of new things in this workshop.
Now, this assumes that you have Node installed because this is a JavaScript application. And we’re going to be using Vite, it’s a newer tool just for bundling applications and making them run, and I found that very useful. This application is a vanilla JavaScript app, we’re not using any frameworks.
And this is important because we want to keep things simple. And also we want you to learn skills that you could apply to any framework that you’re using with XState, whether it’s React, Vue, Svelte, Angular, any of those frameworks, or even no framework at all, if you’re applying XState in different contexts.
So of course, make sure that you have Node installed and NPM installed as well. To get started, you are going to run npm install, or if you’re using Yarn, you could run yarn install as well, or just yarn. And then you’re going to run npm run dev. This is going to start the web server at localhost port .
And so when you do that, and I’ll do that real quick right now. So you just go to, all right, we’re in there, so you’re going to run yarn or npm install. I’ve already done that, so I’m just gonna run yarn dev. And so this is going to run the site at localhost .
So if we go to localhost , You’re going to see the main page here. Now, let me show you in the repo where this lives. If you git clone the repo and bring it down to your local system, you’re going to see in VS Code, you’re gonna see a whole bunch of folders.
Each of these folders contains files for the exercises that we’re going to be doing, but the main file that you see over here lives right here in the main repo. So this is the index.html page, and you can use this as a scratch pad. And so I really encourage you to do so.
We’re going to be using this a lot for just going over the various concepts in XState and also playing around with some code before we jump into the exercises. So just to test that everything’s working, if you console.log a nice Hello world!, then you should be able to see it right in the console over there.
And that’s how you know that things are working. Vite is also really good at doing fast refresh, so keep that in mind. When you make changes to the files, you don’t have to manually reload, you could just see it live in the actual window. Now, what we’re actually building today is a real world app.
We’re building a media player. And so if you go to the last exercise, which is Testing, you’re going to see this media player in action. Now, this is just one of your typical media players. It doesn’t actually connect to media because we’re not trying to play audio over here.
But it’s a good starting point for if you do want to create a media player that does play media, either through the audio, the Web Audio API, or some other mechanism, then you could use this exact same app logic that we create in XState to do so. This app has a number of different functions.
You could like or unlike a song, dislike it, you could play or pause, of course, you could skip forward, and you can mute and unmute the microphone. This is also an app that’s meant to be incomplete, which means it is really what you make of it. Because while we do have certain functionality that we’re going to be building here, the functionality that you decide to add is something that can be added on after the fact.
So if you wanted to add advanced audio controls like changing the microphone volume, or if you want to do things like go back and go forward, or maybe display a playlist. This app is meant for you to really experiment and see all the different things that you could add, and also realize how easy it is to add once you have the app logic modelled as a state machine.
So really, this is something that is for you to play around with and is not meant to be % complete. Rather, each of the exercises are meant to teach different parts of XState, and state machines, and state charts, and parts of the actor model. And like I said, it’s up to you to build on top of those things.
1 | git clone https://github.com/davidkpiano/frontend-masters-xstate-v2.git |
Software Modeling
Software Modeling
What is software modeling?
We’re gonna be starting with our first lesson which is on software modeling. Instead of jumping right into state machines and statecharts, I really want to get down to the why of state machines and statecharts. And why you would actually want to use these types of things when the way we’ve been coding really didn’t touch on state machines or statecharts for however many years we’ve been coding.
But why it’s important today and should have been important the entire time we’ve been developers. So the first question is what is software modeling? Software modeling in short is the art of planning ahead, at least in my own words. Really, if you look up what software modeling is, you see that it talks a lot about abstractions.
And that’s really what software modeling is about is creating a layer of abstraction that is one degree separated from the code. So the code is the implementation layer and the model is the abstraction layer. Now, there are many ways to model software, one of the most popular ways and useful in my opinion to do so, is by using diagrams.
So, you might have started planning software in the past by just grabbing a whiteboard or maybe a pencil and paper and drawing user flows or flowcharts or diagrams like entity relationship diagrams. And just trying to understand what your code is supposed to do and accomplish at a higher level.
And so this essentially is what software modeling is. It’s creating these abstract models that we could code against. However, something that I see way too often in software development is jumping straight into the code. So we’ll look at something like a user interface of something we’re supposed to implement and one of our first reactions says, okay, let’s start coding the user interface.
And then we start adding functionality on top of that user interface by putting a lot of logic and event handlers. And if something needs to be different, then we’ll add an if statements. And this is what quickly creates spaghetti code. So this is something that we really wants to avoid just by planning ahead.
Now, one of the problems or I’d like to call it a friction points with diagrams is that diagrams aren’t always up to date with the code. We might draw a really nice architecture diagram or a flowchart describing our code. But once we starts to code, and once new requirements come in, it becomes double efforts to both update the diagram and also update the code.
So that’s what state machines and statecharts are for. And we’re gonna talk about that in a little bit. But it allows you to do both the software modeling upfront and the actual implementation at the same time, and do it in a way that you could change the model and the implementation and they could stay completely in sync.
And so this allows you to add new features, change features, and really understand your software as a whole in one step. Instead of doing it in multiple steps of creating a diagram, then code into the diagram, or even skipping the diagram altogether, which definitely not a good thing.
Event-driven architecture
So state machines and statecharts are part of what’s called events-driven-architecture. And so event driven architecture is about the fact that you have events as a primitive, anything that can happen to whatever app or software that you’re programming is an event. So something that a user does, whether they click something, or swipe something, or type something in, all of those are events.
And by events I mean things that happen which is the literal definition of an event. So when the user clicks a button, that button clicked events is an event that can be fed into this model that we created and so that model can decide what should happen next based on that events.
Now keep in mind that events aren’t just something that originates from the user, but events can also be something that originates from the system or other systems that are interacting with your software itself. For example, if we start fetching a promise, then the user action of clicking a button or pressing answer on a phone to start fetching that promise is an events.
But also the promise resolving or rejecting, those are also events and they don’t come from the user. So it’s important to keep in mind that events encompass anything that can happen in your application, and not just things from the user. So how do we specify behavior in an application?
Specify behavior(given-when-then,etc.)
This is where things like given-when-then and test-driven developments come into play. And these are really loose specifications for how we talk about how our application is supposed to behave. Now, let’s actually talk about the media player for a minute because I want us to start creating a specification on how it’s supposed to work.
And you could see that specification if you go into modeling and this readme. We have a loose text description of what is supposed to happen in the app at each step. Now I actually really like given-when-then. And that’s because when we represent a specification in this given-when-then structure, we already have all of the building blocks we need to make a really solid model of our application.
So, for example, we say we have here a song when loaded will start playing by default. So, we could say that given, and I will bump this up. Let’s just use large over here. Given a song is not loaded yet. When a song is loaded, then the song should start playing.
So now we have everything here that represents just the different building blocks of our application, or at least one small part of our application. So, this given over here is a state, is a precondition, but it could also be represented as a state in our application. So we had this idea of a song not being loaded yet.
And then this one represents an event. So when a song is loaded, so now we know that that loading, or that loaded part is an events that can happen in the application. Then the song should start playing. So now we have a different states. If the states aren’t changing, then this then part is going to be exactly the same as the given.
But user specifications usually specify that the state of the application does change. So we have a bunch of these user stories. The user could also play or pause the song and we could translate that to, for example, given the song is playing, when the user presses pause, then the song should be paused.
So now we have our playing states, our paused state, and the events of clicking the pause button to pause the song. So, that’s why specifying behavior is so useful and this is why we typically do this using user stories because it gives us all of the building blocks of specifying our application.
So is modeling making a list of all possible events that can occur in an app?
Yes, exactly. Like I was talking about before, typically when we start coding an app, we start coding with the user interface and just start shoving events in there. So now that we’re thinking in an event-first architecture, event-first really describes that there’s a bunch of events happening, and our application is going to react to those events.
But it can be really tempting to put all of the logic inside of event handlers for this. And this typically isn’t really the best way to go especially as the app scales up in features. For example, you might be putting fetch logic inside of a form submit handler.
Or you might be having like defensive conditions and if statements inside of a click handler that determines what should happen when this button is clicked. For example, you might say if the form is loading and the submit button is clicked again, then we might have an if statement, like if, if is loading, then do nothing, otherwise start fetching the data.
And this makes the behavior implicit. It makes the behavior such that we’re describing the application in terms of when this event happens, do this, but also keep in mind these conditions. And so now instead of thinking in terms of like the entire state of the application. Or thinking in terms of okay, let’s have an event just do something and if that’s not doing the exact thing we want it to, add more if statements in there so that we get to do the behavior we want.
And so that’s why I encourage you to take a step back into a state-first approach instead. And so I’m going to demonstrate what we mean by this. And this is also going to get into state machines and statecharts as well. So first, when we look at this, this specification, we have a state-first approach already over here.
We say when the song is loaded, then the song should start playing. But we have this precondition over here too, given a song is not loaded yet. Now, we can make another user story that says, for example, given a song is playing, when the pause button is pressed, then the song should be paused.
All right, so this specification allows us to prevent any unintentional behavior of, for example, if the pause button is pressed when the song is already paused, if you have a pause button visible. Or if the play button is visible while the song is already playing and the user happens to press it again.
Some unintended behavior might happen if the exact same button is pressed more than once and we’re attaching logic to the event handler instead of considering a state-first approach. And so that’s why this given part of the given-when-then statements are so important. And so like I was saying, this gets into what state machines and statecharts really are.
State Machine in Vanilla JavaScript
Event-first vs. state-first
So I’m going to go into the scratch pad over here, and we’re going to start by creating a very simple state machine. That’s doesn’t need any libraries, it’s just, we’re going to be using just a switch statement and we’re going to be modeling a simple prompt. So I’m just going to have over here, function, let’s just call this transition.
And so this is going to describe transitioning the states or describing what the next state should be given this current state in events. And we’re going to be describing like I said, a simple fetch flow, so we have state and event. And so, typically we would start by saying or by having a switch on the events dot type.
So this is assuming that the events looks like type FETCH or it could have other payload like that in the states. It doesn’t matter exactly what it looks like but let’s assume that the state looks like states, we have data, null, error, null, things like that. All right, so when we start switching on the event type, we might do something like case FETCH.
And then we could say console dot log, starting to fetch data, break and defaults break. Okay, so I’m just gonna say, window dot transition equals transition. And we’re going to attach this directly to the window, so we could start playing around with it. And I encourage you to do the same thing if you’re playing around with things in the scratchpad as well.
1 | // main.js |
So we could transition again the state doesn’t matter right now, but let’s say that we have a type of FETCH. Okay, so now it says starting to fetch data, but the immediate issue here is that. The user or even something else in the system can send that fetch events multiple times and it begin to start fetching data repetitively.
And so we want to avoid unwanted states and unwanted actions like that. So that’s why, let’s just assume that we have a status over here. So let’s say that we have status of idle and so we have starting to fetch data, break. It’s something that we would typically do, well actually first let’s change the state.
So we’re going to return status loading and we’re going to see if that’s and just as a follow through, we’re going to return the state.
1 | import './style.css'; |
Okay, so now, if we transition status, let’s say that we’re idle and we send the FETCH events.
Okay, so now it says status loading, but what happens if the status is already loading?
It’s going to fetch data again and so this is something that you might immediately see if you start clicking a button multiple times. So your first inclination would be probably to have a little bit of defensive logic to solve this problem. So we would say, if state dot status is not loading, Then we’re going to start to fetch data.
1 | import './style.css'; |
So now again, if the status is loading and we sent fetch, nothing happens.
And so this is the first step to starting to think in terms of states first. So instead of switching on the events type first, what we really want to do is switch some finite state.
So we could use the status in this example and say switch state dot status, and then say case idle. And now we could do our same fetch data over here and so case loading. We could have maybe some other behavior, break and then defaults, break. And then, now what you could do in here and this might look a little bit ugly.
But you could switch on the events dot type if you have multiple events. Or if you’re only handling a couple of events per state, what I’d like to do is just put it in an if statement. So I say, if event dot type is fetch, then we’re going to start loading the data.
And then we’re going to return that status, otherwise, we’re just going to return the state.
1 | import './style.css'; |
And so now, we’re going to see that if we have idle over here. It’s going to start to fetch data and it’s going to return our next date of loading. And so now, when we load it again, like I showed you over here, it’s not going to do anything.
So keep in mind the difference between doing this and taking a state first approach and taking an events first approach. With the state first approach, we don’t need to have defensive logic, again this could just be a switch statements. And we don’t need to cover up any impossible transitions or states that might arise by doing things events first.
And so the big reason for this is the way we define finite states. Finite states in state machines and state charts, what these represents are behaviors. And so, a behavior is how the application is going to react based on an events. The example I’d like to give is, as humans we’re either asleep or awake and those are two different behaviors.
So we could call those states, I’m either in the asleep state or the awake state. And the reason that I would distinguish between those two states is because. I’m going to react differently to events when I’m asleep rather than when I’m awake. And so that should be a good guide for thinking about like how to separate the different reactions to events in your application.
So for example with this fetch logic or separating the idle behavior from the loading behavior. In idle, when the events fetch is sent, then we should start fetching data. In the loading behavior, when the fetch event is sent, nothing should happen. So, that’s a good way to distinguish what the different behaviors are.
So, one more thing before I get to the exercise. We could also represent this state first approach, which is actually a finite state machine. We could represent this as an object instead and again, we’re not going to be using any libraries. I’m just going to be using an object as a lookup table, so I’m going to call this const machine and I’m going to give it an initial state.
So remember, I’m just using this as a pure lookup table rather than something. That we inject into a library, which we’re gonna be doing in the next lesson. So let’s say I give the initial state of idle and so now I have a bunch of states, so I have idle and I have loading.
So inside this object, I could specify that on the FETCH events, we should go to the loading states. And we’ll just keep it simple for now and keep it like this. By the way, the reason that I’m not just putting fetched directly on the object. Is because we might want to, or in the future, we’re going to want to add entry and exit actions and other things.
So this object doesn’t just represent transitions, but it could represent other things as well.
1 | const machine = { |
All right, so how do we use this machine to actually work the same way as this transition machine over here. I’m gonna call this transition, and let’s give it a states and an event.
So, now instead of creating this big switch statements, we could just look it up on the object. And we could say const nextStates equals the machine dot states. And we’re going to be grabbing the, let’s just grab the state over here. And remember we’re putting it on state dot status, the finite state and then on which might or might not exist and the events dot type.
Now, if this doesn’t give us a state, like if it’s an event that we don’t handle. Then we’re just going to return the current state and then we could return status, next status, just like that. So window dot transition equals transition and let’s test it out. So I’m also going to be exporting or sharing the machine as well, so window dot machine equals machine.
1 | const machine = { |
And so if I console dot log the machine, we’re going to see that we have our machine over here. And then transition, now we could build up this initial states with status machine dot initial. Because we have it over here in a nice convenient property, and then we’re going to send the events.
Again we are going to be sending a FETCH event and so what happens is, it takes us to the loading state. So this is going to work exactly the same way as our switch statements. And it’s going to default to staying at the same state if there are no transitions to find for that events.
So whichever way you decide to write it without a library, whether you’re using an object or a switch statements, it’s up to you. The question was, is the transition function a reducer? Yes, the transition function is essentially a reducer. So if you’re used to Redux or Vue X or NGR X or any of those other libraries that make use of these reducers, then it’s pretty much the exact same thing.
Some of the differences though, are that while it does return the next date in response to a state in an event. It could also contain other things too which we’re going to be seeing in the X state. And it sort of has this specific structure where this state is going to have some sort of field that represents the finite states of the machine.
And of course it could represent other things as well but conceptually you can think of it as a reducer. So again, going over the building blocks of what a finite state machine is. Now that we seen it in code, let’s take a look at it visually. So we have the idle state and that was our initial state.
So, we also have a symbol that represents that this is an initial state, now we also have loading. And so you could see these states are represented by boxes. So now if we want to describe how the idle state moves to the loading states. In state machine notation we would just draw an arrow like this and then we would add the events that causes the transition.
So, in this case, we have a fetch event. So now the graphical representation of this logic is exactly the same as we’re expressing it in code. Whether it’s using the object or using the the switch statements. And so to follow this, you would first look for the initial state in this case it’s idle.
And then depending on what events the machine receives, you would follow the arrow. So let’s say we’re in idle and the fetch event happened. So we’re going to follow this arrow from idle to loading because that’s where the fetch transition takes us. So now if we get a fetch event in the loading states then nothing is going to happen.
And the reason nothing is going to happen is because there’s no outgoing transitions from the loading state. And so this is exactly what the finite state machine really helps with, is preventing impossible states and transitions. If instead we had this arrow just floating randomly and going to the loading state.
Then there are the chance that we could have unintended transitions. But because this is a well defined visual formalism, we know that it’s impossible. For a fetch events to do anything when we’re in the loading states, just looking at the graph we could see nothing should happen.
State Modeling Exercise
We’re going to be jumping into the first exercise. And so, if you go to localhost, port 3000, /00-modeling/
, you could also access this by clicking the link over here. You’re going to see a media player, and this doesn’t exactly need to be hooked up just yet. It just serves as a visual guide for now.
What we’re gonna be mainly doing is working in the console itself. And so, for this exercise, what I want you to do is, you’re gonna see a main.js file. We’re going to be creating a simple state machine that represents the loading, play, and pause functionality of our media player.
And so, let’s first create the state machine visually over here. So instead of loading, or sorry, instead of idle, our initial state is going to be loading. And this represents loading the song, we’re going to have a loaded event. And so, according to the specification, when the song is loaded, we’re gonna start playing it.
So the state machine goes to the playing state. Now, we could go back and forth between the playing and paused states using events. So we could go from playing to paused when we pause it. And we could go from paused to playing, When the play event happens. So basically, I want you to represent this state machine in code without using any libraries.
And yeah, so you could use either a switch statement or an object. And just like we did by attaching the transition function to the window, play around with it, look in the console. Make sure that all of the transitions are actually working. And then if you want to, find a way to localize that state, so you have this object that you could just send events to.
If you don’t get that far, then that’s okay, we’re gonna be doing it together, cuz that’s gonna be an important concepts later on.
State Modeling Solution
Let’s go over how we would represent this state machine using either a switch statement or an object in code. So just like we did for the simple promise example, we’re gonna create a transition function over here and we’re gonna have a state and an event. And remember too that this state is going to look something like status ‘idle’ or status ‘loading’.
But just to keep with the way that we’re doing things in XState, I’d like to have it just as a value. And so I consider this the value of the finite state in the machine. So remember, instead of switching on the event, we’re gonna be switching on that finite state.
So switch and then we’re gonna say state.value. And so now we have three different states that we’re going to be considering. We have the loading state, loading, playing, and paused. So case ‘loading’, case ‘playing’, and case ‘paused’. Default break. Now, talking about impossible states really real quick, these are really the only possible states that we should have.
So if you really want to, you could throw an error to make yourself feel better and say this should be impossible. But, yeah, using XState or other libraries, it prevents this too, because it ensures that only these possible states happen in your application. Okay, so now, we’re going to just return the state, just to make sure.
So in the loading state, we see that only one event can happen. We could have a loaded event and so we should go into the playing state when that loaded event happens. Instead of using a switch statement, cuz it’s a little bit too verbose, we’re just going to have an if statement, if event.type === ‘LOADED’, then we’re gonna return the state where the value is playing.
And so we’re gonna be doing that with the other ones as well. So if the event type is pause, then we’re going to go to the paused state. If the event is play in the paused state, we’re going to be going back to the playing state. And so this is our entire switch statement over here representing all of the events, states, and transitions, the three main building blocks for building this finite state machine.
But we also do need an initialState too. So const initialState=, and we’re just gonna have value, this is going to be loading. In fact, we could do something that I believe Redux does, where if we don’t provide the state or just have this explicitly undefined, just say that this is going to be the initialState.
So now, let’s attach this to the window, window.transition=transition, and let’s play around with it. So we’re gonna go back to the media player over here.
1 | import '../style.css'; |
And let’s first try the initialState and say type ‘PLAY’. Now notice I’m intentionally doing the PLAY event to show you that it should not be handled in the loading state because we’re still loading the song.
So you see that the value is still loading just like we expect. But now if I have loaded, now it goes to playing, again just as we expect. And so if I take this and put this over here, and now that we’re in the playing state, let’s say play happens again, of course, it’s still going to be playing.
And if I have the PAUSE event, it’s going to be paused. And so that’s how you could do it using a switch statement. But there is another way in that by using the objects notation as well, which I find a little bit simpler and it comes with some other benefits as well.
So I’m going to be making a machine over here, providing the initialState of loading and again, we have three states. We have loading, playing, and paused. So in the loading state, on the loaded events, we go to playing. In the playing state, on the PAUSE event, we go to paused.
And in the paused state, on the PLAY event, we go to playing. And now we could take that same function that we wrote in our scratchpad for just looking things up on this machine, and I’m just gonna copy it here from the final.js. Just gonna copy this. And I’m going to call this machineTransition.
And I’m gonna be using the same technique that we used before and just give this an initialState of value machine.initial. So now it becomes configurable, and we’re going to be looking up a couple things. First, we’re going to be looking up that state object based on the value.
So for example, if the value is loading, we’re grabbing this entire object, and then we’re going to be looking up the OnProperty and seeing if there are any transitions. Remember, it might not be defined, so we’re gonna put this optional accessor over here. And then we’re going to look up if there is a transition on that event.
Now, if any of this is undefined, which is shown over here, if we don’t have an XState value, we’re going to stay in the currentState. This is how state machines work. If events are not handled in the states that we specify, or that we’re currently in, then it stays in the same state.
So just consider that a feature of state machines. So window.machineTransition=machineTransition.
1 | const machine = { |
And so, this is going to behave exactly the same way as before. So, let’s say we have transition, let’s call this machineTransition instead. And playerMachineObject, aha, that’s because I’m into this machine. There we go. So now we go to playing, just as expected.
And again if we copy this over, we have machineTransition and we have a type of PAUSE, then this is going to go to the paused state. And same thing if we’re in the paused state and the PLAY event happens, now we’re going to be going to the playing state itself.
Now, you could get creative with this, this is just a pure function. And pure functions, as awesome as they are especially if you’re in love with functional programming. Not having side effects or way to persist states doesn’t really make your application that useful. So thankfully we could use this pure function in a way that we could persist the currentState.
And like I said, you could get really creative with this, I’m just going to have a really simple implementation here. So I’m just gonna say let currentState and that’s just going to be our initialState of value and machine.initial. And then I’m going to say const, we’re gonna call this a service.
And so this service is going to allow us to send events to it. So we could send event. And so we’re going to say the currentState should be the result of the currentState and the event that was passed in. And we’ll also consol.log, the currentState, just to make sure it does exactly what we expect it to.
So window.service=service. And again, this is a very simple implementation, put this in class, have fun with it, make it subscribable, do whatever you want. But the main point is that now it’s something that’s live. We’re persisting the currentState just in this local variable over here, actually it’s a global variable.
And we’re also imperatively sending events to the service. So, let’s try it out.
1 | // ... |
Service.send, so again we’re in the initialState right now. So if I send type of loaded, we’re gonna get playing. And so again because my state is persisted, if I do PLAY, it’s gonna stay on play.
If I do PAUSE, now it’s going to be paused. So I could send a gibberish event, and because that event is not handled, it’s going to stay in exactly the same state.
And so that is a solution to the exercise. Now, also, two things I wanna mention. First of all, if you do wanna go to the solution of the exercise and see it, and I do encourage you to do so if you get stuck, you could always take a look at the final.js file.
And in the actual application HTML, you could uncomment the main.final.js just to make sure that you see the final result. And you could make sure that what you’re doing matches up with that final result.
State Machines Q&A
One question was, can we say that creating state machines like this is the JavaScript implementation of the good old state design pattern? And so the short answer is yes. The state pattern, if you remember your gang of four design patterns, the state pattern is very similar to a finite state machine.
So in the state pattern if you don’t know, a state pattern defines an object that can change its behavior. And so for example, in this application, if we were to represent this as a class, we might have a loading behavior, which is going to be a separate function that describes this is what should happen when I’m loading.
But we’re also giving it the ability to change behaviors as well. One good way that you could do this in your app and sort of get closer to that state design pattern is by separating this in a function. So let’s say we have this case, let’s go over here.
1 | const initialState = { value: 'loading' }; |
So we have this, if event type is loaded, return the playing state. Otherwise, we just want to return the normal state. And so we could have something like function loading behavior in which we know that if we’re in this loading behavior, we’re going to have the state value as loading.
And so instead of having to switch on the state, we could just pop this function in. And so we could just cut this, put there, otherwise return the state. And so now over here we could just return loadingBehavior(state, event). So like I said, this gets closer to that state design pattern.
The only difference between this state’s design pattern and using a finite state machine is that the state design pattern says the machine, or sorry, the class or object, or whatever you wanna call it, can change its behavior at will. With finite state machines, the only way a behavior can change is due to an event.
And I promise you that’s a lot more flexible than it sounds. But it’s a good constraint to keep in mind, that you shouldn’t be arbitrarily changing behavior, but instead make it due to events. So yea, if you want to refactor this as a separate behavior function, then that’s definitely one way to do it.
Does XState allow you to import state machines written in SCXML as XState compatible state machines and export them to, or as SCXML state machines for state machine related tools in libraries? So yeah, that’s jumping ahead a little bit. But by SCXML, this is a a specification by the WC.
And it’s a state machine notation that is represented, as you probably guessed, in XML. And so this standard, if you read it all in, it’s a pretty big read, it is going to feel very close to XState, and that’s on purpose. XState implements SCXML and the algorithms. And one of the long-term goals of XState is to be compatible with SCXML in general.
Currently, we do import SCXML internally for a lot of testing, but there aren’t public-facing features that allow you to do this easily. But it is definitely possible. So the takeaway from this question is, if you really want to understand what XState is about more in-depth, I really recommend you read the W specification for state chart XML.
You’re going to find a lot of things in common with XState and this SCXML notation and see inspiration for a lot of XState’s API. The question was, is there a reason to name the events in all caps, or as I like to call it screaming case? The short answer is no, you don’t need to do it.
I like doing it just because it’s a visual differentiator between states and events. I know something is an event because it’s something that’s screaming at me. So I could just quickly scan the page and see, that’s an event because it’s all in uppercase. But there is no reason that you have to do this.
And even myself, I’m finding myself writing lowercase events, like pause, more frequently in my applications. I think the uppercase events name was just something that was started by libraries and Redux, and it became sort of an unofficial convention. If you don’t wanna use uppercase, you don’t have to.
It’s not a restriction in XStates or pretty much anything. There’s nothing that’s checking that all of your state names are uppercase. However, I will say that for future versions of XState, what you should do is like, if you have events that can really be described within a group of events, so let’s say that we have audio paused.
I really recommend that you separate it with a dot. And that’s the one convention that’s actually going to be really useful. Because in the future, and this is XState version , not XState version , you’re going to be able to specify wildcard events. And so this basically handles any event that starts with his audio dot.
And the delimiter is the dot, so this is also specified in SCXML. So that convention I would recommend, the uppercase convention, you can have your own preference on that. There was a comments that we do have a Stately Discord. And for all questions related to state machines, state charts, the actor model, events-driven architecture, software modeling, and also the tools that we release, it’s at discord.gg/xstate, open invitation.
I highly recommend that you join this Discord because we are extremely responsive in there and wanna help everyone with all their problems. And there’s also so many good ideas and sources of inspiration in the Discord. So if you have any questions or you just wanna chat, feel free to join the Stately Discord, again, at discord.gg/xstate.
The question online was, what about naming the actual states over here? How do we determine what these states should be named?. And so for the states, I have two general guidelines. First, you should name it based on what is happening. And you should also name it based on what the behavior should be.
So as long as you choose any of those, or ideally a combination of both, you should be safe. So for example, in this state we’re asking, what’s happening? Well, the song is loading. And now we’re asking, what’s happening over here? The song is playing. And we’re asking, what’s happening here?
The song is paused. If we were to model something else, like, for example let’s say that we’re modeling drag and drop, but we’re locking to the x-axis or y-axis so you can’t move it freely. You could only move it on one of the axes. Then we might have something like, I call it x-axis lock.
And so now, this isn’t exactly describing what’s happening, it’s describing a behavior instead. So this x-axis lock is a state that says any event that happens here, we have to lock any positional changes to the x-axis. So again, this is a demonstration of what’s happening or what the behavior should be.
And so that’s how you should describe your states. Can a state receive multiple events at the same time? How does the state transition to the next state? And so with state machines and state charts, the idea is that you always receive events one at a time. So for example, let’s say that we spammed this machine in the loading state with five instances of loaded, all being sent at the same time, for whatever reason.
Let’s say that one of our services is just behaving badly and has some race conditions or something. And we just get multiple events at the same time. So what should happen is those events should be queued up. So we have a queue of events and we pop from the queue each time and we say, okay, let’s handle these events one at a time.
So in the loading state, we’re only going to handle one event, which is loaded, we transition. And once we’re on this state, this all happens immediately, by the way, once we’re on that state, then the next loaded event will do nothing. Because we’re in the playing state and the loaded transition does not exist on that state.
So in summary, events are handled one at a time, always.
State Chart XML (SCXML): State Machine Notation for Control Abstraction (w3.org)
XState
Getting Set Up with XState
In the first exercise, we learned how to model this simple state machine using either a switch statement or by using an object. And so now we’re going to be taking this to the next level. And specifically focusing on this object, and using this to actually implement a state machine using the XState library.
And so if you go to xstate.js.org/docs, this is the XState library. And the installation and quick start are all on the homepage over here. So you could npm install xstate, and then you create a machine using createMachine. And you interpret the machine, which makes a live version of that machine, which persists state.
And is something like that object we talked about in the first lesson, where you could send events to it. And so when you create a machine, the object syntax that we put inside of here is going to look exactly the same, or at least pretty close, to what we did in the first lesson.
Now the quickest way to get up and running with XState is actually by using the Visualizer. So if you go to stately.ai/viz, you’re gonna be greeted by this homepage, where you can see an example or just start coding right away, which we’re going to do. And so if I create a machine, I’m actually going to copy this entire object, and I’m gonna put it inside of createMachine over here.
Let’s see, const.machine = createMachine. And so when I press Cmd + Enter, or if I click Visualize over here, now you’re going to see the state machine visualized in the visualizer. And so it’s going to look pretty much like the diagram that we just drew over here. So we have a loading, playing, paused.
And notice we call this AUDIO.PAUSED, it could be paused. And so we could actually navigate through this machine, and show how each event is going to affect the machine’s states. So this is a really useful tool, where you could just copy and paste your machines, whether you have it in JavaScript or TypeScript.
1 | import { createMachine } from 'xstate'; |
And I definitely encourage you to try it out. So, again, if you wanna get started with XState, you would do npm install xstate. And we’re are going to be using the createMachine and interpret functions from XState. And so, yeah, just gonna show you how that works real quick.
We’re gonna be jumping into exercise number one, just to show you how all of this works. And actually, exercise one is all ready done, so yeah. But if you wanna do it yourself, then please feel free. What should really happen over here is like this should be empty, so yeah.
Okay, so for this exercise, what you’re supposed to do is create a machine using XState that matches the state machine that you created in the previous exercise. So just to give you a quick intro on that, I’m gonna jump into the scratchpad over here. And let’s clear some stuff out.
All right, so we’re going to import a couple things from Xstate, gonna import createMachine and also the interpret function. For now we’re just gonna be using createMachine. And so we’re gonna be creating a machine using createMachine, saying that a lot. And this machine, the object inside actually doesn’t need to take anything.
This is a perfectly valid state machine, even though it’s completely empty. So if we console.log the machine.initialState, you’re going to discover what exactly is in that state objects that we have.
1 | // /root/main.js |
So the initial state is created by, let’s have initial state of loading, so states, loading. All right, so our initial state is going to have a few important things.
1 | import './style.css'; |
First of all is the value, just like we talked about, this value over here of loading. And so this represents the finite state of the machine. There’s also going to be an event over here, and that represents the events that caused the transition to this state. In this case it’s just an xstate.init event, which is an internal event.
And there’s also going to be context, which we’re gonna be taking care of in a future lesson. But for now this value is the important part. And yeah, so when we have this machine inside createMachine, we actually have the transition function built into this machine. So we could say const nextState = machine.transition.
And so just like we did before, we could specify undefined as the first argument, and we could give it an event. So let’s say that we have two states, loading and loaded. So we say on LOADED ‘loaded’, that’s actually pretty redundant. Let’s call it SUCCESS just for the sake of this example.
And so we’re gonna send type ‘SUCCESS’. And we’re going to console.log the nextState.
1 | import './style.css'; |
All right, so now we see that we have a state with value of loading. And when we send that event, now we have a state with value of loaded. So we see the event in the state object, and we see the value of loaded also within that state object.
Now this state object has a couple of other features as well. So one feature that’s going to be useful in our application is matches. So if I say console.log(nextState.matches, and we say loaded, this should be true. And so this gives us an easy way to match and make sure that we’re in the expected finite state.
So we have true over here. If we have something else, then it’s going to be false. So you can use nextState.matches. Just like we did before in the first lesson, we created this sort of object that lets us send events to the machine and also we have a way to persist the state.
There is a built in way to do that in XState as well, and that’s by using the interpret function. And so instead of this, we’re gonna be creating what’s called a service. And again, this is just convention. But this service is going to take that machine that we created, and we have to call start in order to start the service, and say, I’m ready to accept events.
1 | import './style.css'; |
Now, we could listen to events on the service by calling service.subscribe. And we are going to console.log the state.value. And we’re also going to expose the service on the window, so window.service = service = service. All right, so right now the service immediately told us that we’re in the loading state.
So if I send type ‘SUCCESS’, we called it, now it’s gonna tell us that we’re in the loaded state, just as we expected. So now we see that everything is working and it’s all good. So interpreting the machine to create these services is really useful. And you could use this essentially as an event emitter in your application.
And it becomes something that you could send events to, subscribe to. And those two things alone are really powerful, and it’s what’s going to drive the entire media player application.
State Transitions Exercise
Getting back to the first exercise. What I want you to do is if you see anything in the player machine, delete it and start over. I want you to recreate that machine and just get a feel for adding initial states, so initial states and adding all the states in the state object and also adding the transitions in each of those states.
And so you’ll also notice that this is connected to the application. So if we go to states transitions, these buttons, at least a couple of them the play and pause buttons should work. So, ideally, that’s what should happen. We already have the code that hooks that up.
So, yeah.
State Transitions Solution
And so this is really useful for debugging or just getting a glance at what state is my application currently in as it’s running. We also talked about adding the states and transitions into the machine in a very similar way like we did with the previous exercise of having that objects notation.
So, if we go in here. And we remember that our state machine is supposed to look like this. Where we have a loading, playing, paused state and the loaded, play and pause events. Then hopefully you’re getting more used to how to actually add these states and transitions into XState.
You want an initial state at first, so that’s going to be our loading state and then we want to specify all of the states that we have in our state machine which is loading, playing and paused. Now to specify a transition inside the loading state, we put it inside the on object which stands for on this event, we go to this state.
So on the loaded state, the way that we specify this XState, the full way is by giving it an object with a target of playing. And so what this is going to do is it’s going to say if we’re in the loading state and the loaded event happens, go to this next target state which is playing.
Now a shorthand for this is if you have no other actions or things to specify in the object you could just put the key of that state inside as the value instead of the full object. Both ways work both ways are the same. I do recommend you use the object especially if you’re going to add actions and conditions, and things like that that we’re gonna talk about in the future.
So in the playing state, on pause, we go to the paused state. And in the paused state, on play- We go to the playing state. Now below you see that we already have the machine being interpreted and we also have the step tools true thing. And I’ll show you what that does in a minute.
But this starts the service so that it could start receiving events. What we also did, and just for the sake of time, it’s already done for you. We have a bunch of elements that we could access. And so, we’re adding event listeners to each one of them. And so, this is going to be an important pattern using whatever framework that you plan on using with XState, is that the only thing that should really happen inside events listeners is sending an event.
And so this alone simplifies the implementation of the logic in your application and it also allows you to centralize that logic. So now, when we click a button for instance, all that’s doing is being translated to a play events and same thing over here with the pause button, when we click it, that’s being translated to a pause event.
And so we’re also subscribing to the state because we do want things to happen on the screen. So, we want the loading button to be hidden if the state doesn’t match loading. We want the play button to be hidden. And so now instead of using matches we could do something else.
Since this play button is based on an event and not the states, we could do it if we can’t play. So the state machine does make this obvious where if we’re in the playing states then we can’t play because that transition is not defined. And so the state.can method allows you to do this pretty easily.
And say if sending this event is not gonna cause a transition in the state machine, then this is going to return false. Otherwise, if it will cause a transition, it’s going to return true. So it is really useful and you could use either dot matches or state.can to control the UI.
1 | // @ts-check |
So congratulations. And when we go into the actual application- We see that this pause play, pause play it works. And so, again when you’re testing machines or you just want to debug them you could attach the service to the window- And you could send events that way. So, for example, now we can see the loading state and that’s because I commented out this loaded event.
And so if we send the play, nothing’s gonna happen. We’re still gonna be in the loading state. And that’s because we haven’t loaded the data yet. So, if we instead send loaded now that changes to the plays states, and I know it confused me before in the past but showing the pause button means it’s in the played state because it shows like, okay, you could pause it because it’s playing.
So you could click pause, and now it’s in the play state, pause state, play, etc. So if your app in this file looks like this and it behaves like this, then it’s working. So congratulations. And so this is really useful for debugging or just getting a glance at what state is my application currently in as it’s running.
And so this is really useful for debugging or just getting a glance at what state is my application currently in as it’s running. So you could import inspect from XState inspect and then give it a couple of options. By default it’s gonna try to find an iframe and this option might be changing in the future or this default might be changing.
And we also wanna open it up in stately.ai/viz?inspect
. And so what this is gonna do is it’s going to visualize our machine as it is running in the app. And so this is what we call inspecting the state machine. So right now we see that it’s in the loading state.
And so this is really useful for debugging or just getting a glance at what state is my application currently in as it’s running. [COUGH] So we can press pause to pause it. And now we see it’s in the paused state, but one cool feature about this inspector is that it goes both directions.
You could also click play over here and cause the machine to be playing. So it’s bi directional, it’s going to work both ways. And so this is really useful for debugging or just getting a glance at what state is my application currently in as it’s running.
1 | import { inspect } from '@xstate/inspect'; |
Actions
Actions
All right. Now let’s talk about actions. This was also a question that was brought up like how do we express side effects inside the state machine as well. In state machine and state chart terminology in action is a side effect. It is something that is performed as the result of an event.
So there’s three different types of actions that we could specify in those our entry, exit and transition actions. So, one important thing to realize is that actions don’t happen randomly, they happen always due to an events. So for example, if we’re in the loading when we entered the states, and I could just add this over here.
So I’m gonna make this different color, we could say entry load data actually let me make that a little bit smaller if I can [INAUDIBLE]. There we go, so what this is saying is that when we enter this date we want to start loading data and this is a side effects that happens, this side effects might eventually come back and say okay here’s your data it’s loaded.
And then we go into the playing states. So again, we could represent entry and exit actions here too. So we could say, play audio over here because playing the audio is a side effect. We have to tell something external that the audio file actually needs to start playing and when we’re not in this playing states we want to pause the audio.
So this just make sure that the behavior of not only the way that the machine can change its behavior based on events. But also on what side effects are executed, the behavior of that is specified through these actions. Now, there are also transition actions and these are actually the actions that I would recommend you start with.
So a transition action in state machine terminology is specified with do. So it’s not gonna look like this in x states. But when you’re looking at state machine diagrams, it has a do in it, and that’s how you know that this is a transition action. So when you’re starting to specify what side effects should happen in when they should happen.
Like I said, I really recommend putting them on the transition first. So for example, when we loaded we want to start playing the audio. And when we pause, we wanna pause the audio but notice that, okay. Over here when we’re paused in the play event happens, we want to play the audio as well.
So now look at these two events over here. These two events are interesting because they are doing the same thing and it’s showing you that any transition that goes into the playing state needs to execute those events. And so that’s how you know that that’s actually a candidate for having an entry event.
[COUGH] So, use entry events when you realize that every single transition to the states results in that action being taken, same thing with exit events or sorry exit actions. If you notice that every single transition out of the states results in this exit action, then or results in this action, then you should convert that to an exit action.
So for example, if we have do pause audio but we realize that anytime that we’re exiting this playing state, this pause audio happens. And so let me just make another example over here. So let’s say that we’re playing. And let’s say that we have this dreaded, gonna color this.
We have this dreaded buffering events, which we’re not gonna model in our actual state machine, but just lets pretend it happens. If we only have pause audio on this pause event, we need to duplicate it over here because we also want to pause the audio if it’s buffering because obviously there’s no audio to play.
And so now we know this that every single exit events, or sorry, yet every single exit transition on the state note is going to cause this same pause audio events. That’s a candidate for instead having exit pause audio and removing these two. So in summary using entry and exit actions is a way to dry up your state machine logic but starts by specifying the actions on the transitions themselves first.
Alright, so the way that we specify these actions on the states or the transitions is by, here I’ll add over here, is by specifying it in line at first. And so this is going to be the first thing we’re doing over here. I’m going to go to the scratchpad, okay, so again we have our simple machine we have our loading machine and on success loaded let’s make this a target over loaded.
So in this state objects, we could specify entry actions and we could specify multiple actions as well. So, an action can either look like a string, so we could say, load data, or it could be an object. In this way, it allows you to have parameters, just whatever you want.
But in this case, we’re just gonna have it as a string just to keep things simple. All right, so now, if we go back to the homepage, we’re gonna see that it shows loading over here, but I also want to log the states.actions. And so now we’re going to see that we entered the loading state and now we have an array of actions.
Notice how it converted it to an object over here we have a type of load data. And that’s gonna do something we haven’t specified what it’s going to do yet, but that shows us that this action is meant to be executed. Now, the state machine transition function will not execute actions, the interpreter will and that’s one of the roles of the interpreter is not only persisting the current state, but also executing actions too.
So if instead we change this to an inline function. And we say consult.log loading data. Then you’ll see here that this loading data message appears because we’re entering that state and executing this action which it’s going to console dot log it out. And so we could do the same thing for transitions, so I could have an action over here, actions.
And I’m just gonna do an inline action again, and say console.logdataloaded. Or I might do something like assigning data which we’re gonna be doing in the future lesson. So now it says loading data. And if I send type success, that just shows you the same machine is working.
Now we see it says assigning data. And that’s because it’s executing that inline transition action. Now, you as a refactor target, you don’t have to specify all of your actions in line. Instead what you could do is, for example, let’s have load data, the machine itself has a withconfig method that allows you to specify the implementation details of these actions and other things as well.
So if I put actions as an object here, and we say, load data, And then we console.log configured loading data. Then we should see that this configured loading data message shows, and that’s because it is reading from these implementation details. So those are the ways to specify actions either in Entry Exit or transition actions over here and I do recommend that you specified them as an array.
Because there is always the chance that you might need to execute multiple actions inside your transitions or in entry or exit other states.
1 | import './style.css'; |
Actions Exercise
Inside this exercise, we have a pretty fleshed out player machine. And so, this player machine is doing what we did in the previous exercise, but we actually need it to start doing stuff. And we don’t know exactly what the implementation details of that stuff is gonna be.
So we are just going to put some stub actions in there for now. So on the loaded state, we want to add an action that assigns the song data. And we’re gonna cover what that means in the next lesson, but you could just add a console.log there. And then we’re going to be adding two actions in the playing state.
So whenever the playing state is entered, we need to add an action to play the audio. And so when the state is exited, we want to add the action to pause the audio. Now, we have a few more actions over here, and this is something I wanna talk about too.
That you could specify this on object on the routes of the machine. And so what this is going to mean is that these events should be handled, no matter what state you’re in, unless they are overwritten by another states. But yeah, so for now, just realize that these events can be handled whether you’re in the loading, playing or paused state, and we could customize that behavior later.
So in the skip states, we’re going to skip the song, so again, just make a dummy action that console logs I’m skipping the song. We also have the option to like or unlike a song. These are also actions that don’t go to a specific state, but they’re just side effects that are done.
And we have a dislike action, now this is actually interesting because if we go to, I think it’s testing, you’ll notice that, sure, we could like a song, but when we dislike a song, it needs to do two things. It needs to mark the song it’s disliked, and also skip to the next song.
Because, obviously, if a song is disliked, we want to stop listening to it. So this is where the actions array is going to come in handy. So we also want to add an action to the volume. And if you want, and if you have time, specify the actions in withConfig over here.
Now there’s one more thing that I want to mention for this exercise. And that’s that you can also raise an event. And so this is an example of a built in action in XState. So we’re gonna import raise from xstate/lib/actions. And so, this is actually a really useful technique, because when you raise an event, that event is sent to the machine itself.
It’s like you as a person telling yourself or reminding yourself to do something, same thing with the machine. The machine can tell itself to do something. So, for example, I’m just gonna make a BLAH event over here, this is just a demo, so the names don’t really matter.
I’m going to raise a success event, so type SUCCESS. And so now, When I’m in loading, and I send, instead of type SUCCESS, I send type BLAH, it’s gonna go to loaded. And the reason it goes to loaded is because it’s going to execute this transition and one of the actions is to raise this event.
So this event is going to be sent to the machine itself, and it’s going to now execute this transition. So the raise action creator is really useful.
1 | import { createMachine, actions } from 'xstate'; |
Click on both STEP
and RAISE
events in the visualizer)to see the difference
Actions Solution
For now, the simplest way to get started with actions is to actually just implement them inline. So for example, on the loaded state, we see that we have to add an action here to assign the song data. And so for now, I’m just going to say actions, and I could specify this as an inline function and to say console.log assigning song data.
And so now when I go to actions, it says, assigning song data because that loaded event is being sent at the bottom over here. Now, what we could do is we could serialize this and say that instead, we just want this to be a named action and call it assignSongData.
And then we could specify it in withConfig over here. So we could say, assignSongData and just have the same thing, console.log assigning song data. And so same thing should be happening. Just make sure, yep, assigning song data. So now that’s something that you could do for the rest of the actions over here.
Of course, an entry action is going to be the same format, just with a different property name. We have entry actions, or sorry, not actions, but just playAudio. And so same thing with exit. Instead of play, we have pauseAudio. So we have assigning song data and, We have this playAudio and pauseAudio action.
We’re not console.logging it yet, but you could see over here that we subscribed and we’re logging the state.actions. So we should see that, okay, when the song is loaded, we’re doing two actions. We’re assigning the song data, and we’re playing the audio. So now if I pause, one action is gonna happen, which is that pauseAudio action.
And again, you could specify the implementation details inside of the actions over here. Now there was one event that is supposed to cause multiple actions. Let’s see how we could do that. So because we specify our actions in an array, we could have something like dislike song. And we could also raise the skip event.
Because that’s a built-in event, we could just specify it right over here, so raise type SKIP. So let’s see what happens if we dislike the song. Look at our array. We have dislikeSong, and we also have pauseAudio. So what exactly happened over there? Well, this skip event takes us to the loading states, and this should actually be .loading.
And the loading state, so I’m gonna dislike it again. The loading state goes out of the playing state, so this exit action is gonna happen as well, and so we have pauseAudio. Now, this really highlights the value of why raise events are useful is because if we were to have skip functionality elsewhere, like we have over here in two places, both with the skip event and dislike.
Now we don’t have to copy and paste that logic. Instead, we could represent it in a more abstract and dry way, which is to say when the dislike event happens, we want to dislike the song and we also want this machine to behave as if a skip event was sent to it.
And so it sends itself a skip event, and it does all the logic there. So now it becomes really easy to centralize this skip logic into this transition. And so now you don’t have to repeat yourself everywhere. So let’s say that we wanted skip to do something else.
We want to add actions. We want to add a condition to the skip. We could do it all in here instead of having to copy and paste all of those different things inside the dislike action or the dislike transition as well. I’m not going to do the rest, but you could take a look at main.final.js and see how it’s done.
One common pattern with XState is that for things that might take multiple objects, you can pass in an array or not. I recommend you be consistent, though, in passing an array just so that it becomes easier for you to add another action if you desire. So looking at the rest of this, it’s all pretty much the same thing.
We have our actions over here. Again, put them in an array if you’d like, which makes multiple actions easier. We have raise SKIP. And because this is an event without payload, we are representing it just as a string. But if you want to be consistent, you could also just provide the entire event object in there.
And then we have a bunch of actions over here, and this is something that we’re going to fill in for the most parts in the next exercise.
1 | // @ts-check |
Actions Q&A
One of the questions asked, was raised, sounds like a way for a child state to send events to the parent states. Can I use it for that, too? The short answer is yes, and that’s exactly what we’re going to be doing in the compound states lesson. And so again, raising an event makes it so that even if the child is handling a different event but it needs the machine to behave as if another event handled in potentially a different state.
But with handle it then raising event basically lets you abstract that away. So updates should not be performed solely on the value of the target, they should only be performed on an action. So I’m interpreting updates to mean what is shown in the UI, and also what side effects are executed.
Side effects which are things that are done externally, those should always be done in actions. Now the way that UI changes, we could have just what we consider to be implicit actions. And react, and other frameworks sort of just abstract this away for you, because anytime we’re changing something in the UI, that is actually an action or side effect.
It’s just that we abstract that away in a declarative view layer, which is what react and view and other popular frameworks provide. So over here we’re just synchronizing the UI to the state, and that’s based on the actual state, like Loading, Play, Pause. Now if we want to get really granular, we could do something like when we enter the loading state, show the Loading button when we exit the loading state, hide the loading button.
But that’s gonna get very verbose very quickly. And so that’s, again, why frameworks exist, and why right now we’re just synchronizing the state with those attributes. So it’s up to you to decide whether you want to represent updates as actions, or as just synchronizing with state. But yeah, just keep in mind that external side effects should be done in actions, or actions executed in sequence will Raise skip, only start executing after dislike song is done.
So, yeah, the way that this works, and, in fact, I’m gonna take this and I’m gonna copy it. And we are going to put this inside the visualizer so that we could just take a look at it. Let’s import Raise from next day live actions. All right, so the way that the actions are going to be executed, for example, when we’re in the loading state, and the loaded event happens, now we’re in the playing states, and let’s say we want to skip this song.
What’s gonna happen is that skip song, because it’s a transition action, that’s going to be executed first. And since we’re exiting the playing states, now the Pause Audio action is going to happen. I’m pretty sure, and also you should not be super dependent on the order of actions.
Assume that the actions are going to be run at the same time even though they’re not run concurrently, just don’t depend too much on the order of actions. If you do want actions to run in sequence, like this action needs to run before this one, then that’s something that should be represented in separate states.
So that it is % clear and precise which actions are going to execute when. Another question, I’m curious why X state has many ways to do the same thing. It seems there’s a preferred way, so why allow so many ways such as inline actions versus using with config, etc.
So part of this is just the evolution of the X state. It’s a state machine and state chart’s library that’s about six years old. So it’s gone through a lot of iteration, and also it’s about the purpose of what you use it for. So X state is meant to be like this hybrid of allowing you to model your states and your logic, and also allowing you to implement it.
So just like we’re doing in this lesson, we’re implementing our actions in the nice and quick way. Like, I honestly appreciate that I could just type in whatever action, and worry about the implementation later. Because this is something that can be immediately visualized, handed off to someone without having to think I actually didn’t implement that yet so I can’t share with you the diagram or the model that we’re gonna be programming against.
So, I also want to say, too, that if X state only allowed one way, it would be very verbose. So for example, actions, let’s say that every action required a type, which this is what it’s converted to, and the actions can have multiple actions inside of them. So this is how you would have to type everything, and so for example this target, or yeah, the target is already playing this, you’d have to put it in an array.
So I do see the benefit of staying consistent, and that’s something that I’ve heard in the past. However, as a prototyping tool, that becomes a friction point. And so that’s why X state does have a couple of different ways of doing the same thing. In the future with X state version and other helper libraries on top of X state, it’s going to make it easier to enforce a single way of doing something.
And also, we are going to be updating the docs and we’re gradually doing so already, where we only emphasize one way of doing things such as typing your actions, or putting your states in. But the key is that we want you to be able to move fast with actually modeling your application, instead of having to drown yourself in a bunch of syntax that you might not need.
Another question in the chat, can you recap what Raise does in how it compares to send? And so that’s something that we’re also going to be talking about in the last lesson, or second to the last lesson, on actors, is that Raise is a machine sending events to itself.
Send is either a machine sending an event to itself, or sending an event to another machine. I recommend you use Raise if you’re sending events to yourself, and you Send if you’re sending events to another actor, or machine which is an actor. So, that’s in short the difference there, and in version we’re going to consolidate it so that Send becomes Sent To, and really becomes an action for sending to a separate actor.
And encouraging the use of rays in order to send in events to the machine itself. And another question, does Raise work with parallel states, too? We’re gonna be talking about parallel states in the future lesson, but yes, Raise works everywhere.
1 | import { createMachine, assign, interpret, send } from "xstate"; |
Context
Context
When we were talking about modeling in the first lesson, we were describing states or finite states in particular as representing the different behaviors of your application. So what do we do with the extra states that we’re used to knowing? So this is sort of the difference between qualitative and quantitative state.
Where qualitative means it describes something like a mode or a status of your app that determines the behavior, and then quantitative more describes things that don’t really have anything to do directly with the states. For example going back to me as a state machine, I can be either asleep or awake and that determines my behavior.
But I could also have different attributes like my height or my age which more or less don’t really affect the behavior directly. They’re not specific ways of categorizing my behavior, they’re just attributes of me. Or my favorite color, my eye color, things like that. So in xstate we represent this so called extended state or state that’s not finite state in context.
And we could update that context using an assign action creator. And then we can read from the context via state.context. So let’s take a look at how this looks in an actual application. I’m going to go back to the machine that I created in our sandbox over here.
And the way we add a context, which is our initial data so to speak for the machine, is by specifying the initial context in the context property of the machine. So I could say for example count . And so now in my app when I’m reading and when we’re going to actually read the context.
When we’re reading the state, we could read the state.context and get that data. So over here it’s logging count , just because that is exactly what we specified in the state.context. Now what if we want to actually to modify that context. Well, we use an action to do so.
So I’m gonna create an assign action here and I’m gonna pull this in from xstate. And then the easiest way to do this is to pass an object in here and the parts of the context that you want to change go in that object over here. So we could say counts, and let’s just change this now to a .
All right, so the count is still . But now if I send SUCCESS, which is where that action is defined, the counts changes to . Now this is changing it to a static value. You could also change it to a value depending on what the current context and the events that was passed in gives you.
So we could increment the counts by saying, just gonna say context.count + . And so now we send SUCCESS, the count is . Especially if we have events with a payload, we could base it on that events payload as well. So let’s say that we have in events.value and so we have to add that payload, so we’re just gonna say value .
So now that’s going to take the current context, count, and it’s going to add to it. And so now you could see the count is . Yeah, so keep in mind that even if you have other properties in here, such as, name David, those aren’t affected by this assign action because in this case we’re only modifying the counts.
So if I add a again, I still have the name of David, and the count is .
1 | // @ts-check |
Context Exercise
So now, we’re gonna be jumping to an exercise on context, and so in this exercise we want to do a few things to actually make our our machine a little bit more useful than something that just toggles between play and pause. So we’re going to add the initial context first for the song title and artist which could be undefined at first.
The total duration of the song which, again, we don’t know so it starts at the elapsed value which is how far along we’re at the in the song which can be , the like status which is a string, and that like status can be either unliked or it could be liked.
Oops, liked or it could be disliked. And yeah, there’s three different values for that. And then the volume which is a number. So you’re going to add that initial context into there. And then we are going to change some of the actions so that they’re actually assigning to context.
The assigned song data. We’re going to assume that it takes an events with type loaded in this data in the payload, and you’re going to assign the title, artist duration, and you’re going to reset the elastine like status values. Try to see how many of these assign actions you could get to, we’re gonna take a short time to do this and if you don’t get to all of them that’s fine, I just want to get you used to writing these assigned actions.
So yeah, in this assign time is another action that’s going to be reading directly from the events, just getting you used to setting the initial context and also assigning it via an action. Keep in mind too, that when we render this out, that context is actually read just like we talked about from states.context and is used inside the application to display certain things, and we’ll talk about that after we’re done with the exercise.
Context Solution
So let’s talk about adding context into our state machine and updating that context using the Assign Action Creator. So the first thing we want to do is add the initial context. And of course we don’t know what the title is. We’re just going to keep it undefined.
So title undefined are this undefined and duration zero, lapsed zero. So you could set these to whatever initial values make sense for your application. Like status right now it’s just unliked and volume we’ll just set it to a default level of . So now we actually want to modify this data.
So just for example in the loaded action we have this assigned song data. So if we go here and we assume that the event looks like this with loaded, we see that we have three things that we could pull from the events that we could assign to context, the title, the artist and the duration.
So Our assign function is going to take both the context and the event. So over here we could just read event.title. And then we could do the same thing with artists and duration. And then we also wanna reset the elapsed and the like status values, where this is gonna be unliked.
All right, so over here we have an example of these values being sent directly to the service. And so if we go over here, we see now that we have, actually nothing quite loaded yet. So Let’s check in just to make sure that all right, yeah, we’re on main js.
And we’re setting that loaded event. That loaded event is going to assign all of that song data. Let’s see. And so, when you’re debugging too you could also just add console.log here like I like to do. [LAUGH] And yeah so, Yeah it says here and so, it should be assigning that song data.
So let’s just make sure it’s getting the right thing. You could also log the context of the events context of events just making sure we’re sending the right event over. So the events is loaded. Aha, yeah so this data is gonna be in the events.data. And that’s just because of how our event is structured.
And so I saw that just by console.logging, and looking at the events and noticing all this stuff is inside data. And that’s gonna be important for later too when we start talking about invocations. So don’t make the same mistake I did yeah. And so now we have the song title, the song artists, the duration loading or the duration there and while this is actually a combination of duration and elapsed.
And you could see down here that we’re actually setting like for example, the scrubber input value to the elapsed time and we’re also setting the inner HTML of this elapsed outputs too. In this case we’re doing elapsed minus duration just because that’s a nice convenient way to combine to combine both of those.
But yeah, you can see that we are reading from context and putting it directly on the screen. And so you’re essentially going to be doing this for all of the actions. And so if you got through a few of these, hopefully at least the first one then you pretty much have the hang of it.
Because they’re all pretty much the same technique. And so when we go to main.final and use that in our application, now you see that we have the like status working. So if you click here that heart should show or the dislike status, and that’s going to actually change to the next song too.
And it’s going to be loading which is why those buttons disappeared. But yeah, so all of that is working so far. And yeah, so you should be able to see data on here, shown from the context. The question was will there be time in the course just talking about updating X dates context with immer.
So one of the other ways that you could assign context is by passing in a function directly. And this function takes the context and the events and it sort of acts like a mini reducer. And so you could return some sort of updated context over here. Now, because you could do this, you could use immer’s produce in order to do the same thing.
This is an exercise left to the reader though. There’s also an x state immer package that I recommend you take a look at. Which has this built in for you but it is pretty straightforward to use the produce function from immer directly in the assign function. And you can even do it with the object syntax over here.
So if I have like title or just something else I could use immer. So I could use produce right over here. So that’s just something to keep in mind if you want to use immer with X date.
Guards
Guards
The next thing I wanna talk about is guards, or specifically guarded transitions. What are guarded transitions? Typically when we have a transition, the transition tells us which states to potentially go to next based on the events, but sometimes that isn’t sufficient. We want to take that transition only if the event of course is the one received, and if we have some sort of condition that is true.
So this is what is meant by guarded transitions. And just to give you an example over here we’re gonna go back into our main state machine that we have here. All right, so let’s say that we have a SKIP event over here, just in our contrived state machine.
And I want this to go to the loaded state, which is over here, so target loaded. But I only want it to go to the loaded state if some condition is met. So I’m gonna say cond for condition, and we’re just gonna be doing this inline. We want this transition to only be taken if the context.count is greater than , for instance.
All right, so if I go to the machine, and we see that we have the context over here, so I’m going to also log the state.value. So you see we’re in the loading state and the count is . So if I send a SKIP event, then you’re going to see that it’s still going to stay in the same loading state.
And that’s because the count is not greater than . So now let’s change that. Let’s say that the count either originally is greater than or another event caused the assign action that we learned in the previous module to change the counts to something above , so .
Now when we send the exact same event, that transition will be taken, and we will get to the loaded state. So assigning guards to transitions can be done inline like this, or just like actions you could specify it as a string too. So we could say greater than .
And then we could add a configuration option here under guards and say greater than and provide that in right there. So now, again, when we try the same event, it’s going to take us to the loaded state. So those are the two ways of specifying a condition over here.
Specifying it as a string is better for visualizing, cuz you can see the name directly in the visualizer. But if you’re quickly prototyping a state machine, then you could easily just specify that condition inline.
Guarded Transitions Exercise
In this exercise, we’re going to be adding a few guarded transitions. And so, these guarded transitions are going to ensure that we go to the right states, and or perform the right actions only if the conditions are met. The simplest one to start with is going to be this volume over here.
So we want to make sure that when we send a VOLUME event, which isn’t in the UI, but you could send it directly to the service, just via the console, we want to make sure that the volume is only between zero and . And so you could add a guard to do that.
Another guard that we want to add is a TOGGLE. And so, this light.toggle is for, I’ll just go over here, guards. The light.oggle is for this heart ,so this is for disliking but we want to be able to send a light.toggle event from this heart that will either like or unlike it, depending on which one is clicked.
If we go back to our main js, I just want to show you that transitions can also be in the rate too. So if this transition, the condition for it is not met, then we could fall through and choose a different transition. So we could say, target, Let’s just having an error state, so error.
And then let me just show you how these works real quick. Let’s move the counts below . And so when we skip, now we’re in the error state because that condition or that guarded transition falls through and it picks the next one, since this one wasn’t met. And these are going to be picked in the order that you specify.
So in the guards challenge, we are going to be doing the exact same thing here for ‘LIKE.TOGGLE’. Now there’s one more thing to explain for this exercise and that’s something called an always transition, or it’s called either an eventless or an always transition. We’re gonna call it an eventless transition, but it’s specified with always, sorry for the confusion.
And so, an eventless transition is a transition that is immediately taken when the condition is true. So it doesn’t need an event to activate, it’s just something that whenever something causes the state to change it’s going to be taken if the condition is met. So, for instance, instead of this skip over here, let’s say that I have an always.
And I’m always going to, if the context.count is greater than , then I’m going to go to the success states. So notice, this is in the loading state, and this transition will now only always be taken if something causes the state to change or like when we initially entered the state and this condition is true.
So, right now, I’m just going to put a success here. All right, so right now, it stays in the loading state, because that eventless transition specified via always is not taken. So now, if I change this to , now you’re going to see that it immediately goes to the success state.
And so that’s why eventless transitions are useful, because they don’t require events to be activated. They’re just automatically taken when some condition is true. So you’re going to be using that in this exercise to go to the pause state whenever the elapsed value is greater than the duration.
Guarded Transitions Solution
In this current exercise, we had to do a few things. And I’m gonna start with the simplest thing first over here, which is making sure the volume is within range. Of course, you could add a condition over here and apply this in line. So, the condition function takes two arguments, the context and the events.
But we want to make sure that this event level, and we’re only focused on the event level but we wanna make sure that that’s between and . So you can do something like return event.level is greater than and event.level is less than or equal to .
And so now, when we run this, Guards, we see over here that the volume is currently . So if we send a volume level of, let’s say , and they have it conveniently over there ready, then we see that the volume changed to , which is great. So now if we send something like , That is going to keep it at .
Now one question might be, okay, why do we have to have a guard here where we could just constrain the volume in assignVolume? And you definitely can do that. Here’s the difference, though. If you allow this transition to be taken, it’s going to basically tell the state machine, this state is changing.
But if the volume isn’t changing at all, then nothing really is changing, so you’re sort of lying about the state changing. So, it’s just a more concrete way of us describing that, no, nothing is changing. We’re not changing the state, we’re not running any actions because the volume is not within range.
And with things like states.can, which is that useful method that tells you if a state is able to accept an event, that is going to be false if the volume is out of range. Because it is going to check this condition and tell you nothing’s gonna happen if you send that event.
Whereas if we had this in assignVolume, then it’s gonna say yes, we do accept that event. But nothing will happen, but there’s no way of you knowing that nothing will happen. So the second part that I want to go over is using an always or an eventless transition to automatically go somewhere when some condition is true.
And so we specify that here using always. And one trick to prevent infinite loops, because always functions, or sorry, always transitions are always gonna be taken, is that you want to get in the habit of adding a condition first. So, add the condition that the context.elapsed is greater than or equal to context.duration.
And so, when that happens, we want to go to the paused state. So now when we run this, and let’s say that right now it is currently playing and we have a minute . And so, let’s say that we have something that changes the elapsed time. And that thing is gonna be audio.time, which is this assignTime here.
So, I’m going to send type AUDIO.TIME and I’m gonna give it a current time, which is what we have over here. That current time is gonna be something really, really high, like , just trigger this. And so, yeah, you have to add service.send. And so now you see that it immediately went to the paused state because it elapsed or the elapsed time was greater than the duration.
And so, yeah, that’s just a useful way of combining both transitions and guards. And using that within always is a really useful pattern because this, again, doesn’t have to be dependent on any specific events. So now if we go to main.final, you could take a look at all of these implemented.
We have this which is always going to the paused state when the elapsed value is greater than the duration value. And over here we also have the guards for LIKE.TOGGLE. So it’s going to check this one first and say, if the like status is liked, and we’re using the raise action here again, we raised the unlike event which is going to cause us to unlike the song.
And if the like status is unliked, then we raise the like event, so we’re just going back and forth. Now, some of you observant ones might look at this and be like, isn’t this sort of a state machine on its own? And the answer is yes, and so this is also an exercise to the reader.
You could also implement this as a separate parallel state once we get to that in a couple of lessons. So, the final behavior over here should be, I’ll just show it to you right here. So yeah, this is the desired behavior, if you’re able to implement it, where it goes between liked and unliked.
And so also we have the guards preventing the volume from getting too loud and also skipping ahead, or yeah, going to the paused state when the elapsed time is greater than the current duration. One of the questions asked was, when we apply the guards, so cond, how does that look in the actual state chart?
So we could visualize that here or here. So let me just make an example over here, so const machine = createMachine. All right, so let’s say that we have a loading state. And on some event, we only want to do something if the condition is true, so loaded.
So we could say cond ‘dataReceived’, sorry, ‘dataValid’, and target ‘loaded’. So this is how it’s going to look and this is how it’s represented in a state chart. We have the event here, some event, and we have the guard here that’s represented using these brackets. And so this is typical for state chart notation.
So whenever you see those brackets, just realize that this event is a guarded transition or is part of a guarded transition.
Compound States
Compound States
Now we’re gonna get into some of the main features of state charts. Some of them we already went over such as guarded transitions, and actions and these are things that can be applied directly to a state machine. Which we’ve been working with up until this point, but are also very useful in state charts.
So, what are the charts and how do they differ from state machines? State charts bring a lot of new capabilities to state machines but at the same time, they can be decomposed into state machines. State machines have this problem where when you have too many states and transitions, a lot of the connections between different states and events and things like that become redundant and also combinatorially explosive.
So let me give you a quick example, just demonstrating why that is. So if we go to xceltra, let’s take this event right here, this buffering events. And let’s say that we’re in either a playing state or a paused state and buffering can happen. But right now it says that buffering can happen in the playing state.
But we know that, the internet connection can go out and buffering can really happen in any state. So, just gonna add another arrow here make it white and we also have buffering happening right here. We can even rotate this if we want, there we go. All right, so this is one of the first telltale signs that we’re not really doing anything dry here, we’re really repeating ourselves.
And we have this transition over here in this exact same transition over here, when really we want to represent that whether you’re in the playing or pause state, any one of those states can receive a buffering event, and that buffering events can cause us to go back into the loading states.
Again, this isn’t the actual machine that we’re going to be using in our application, but it’s just an example. So one thing that we could do, is instead represents the loading state in both the playing and pause state as two different states. So, if I draw like a big state over here.
Then we could group these two states together. And so let’s just move this out of the way. And so now, by doing this, we could actually get rid of at least one of these transitions. And say that we want the buffering transition to go to the loading state regardless of which state we’re in.
And now we could also say that, this loaded transition is going to go to this parent state, which we’re gonna call ready, make that a little bit bigger. And so this ready state contains both the playing and the paused state and what we want to do is also specify that there is an initial state over here, which is this playing state.
And so now this actually allows us to dry up a lot of the logic in our application. So now instead of saying whether we’re in the playing or paused state, if it’s buffering go to the loading state for each of these states. Now we could say in this parents ready states when the buffering event happens, go to loading regardless of what state we’re in here.
So we could add , trialed states here and we don’t have to specify extra transitions. And so, the way that this state chart now works is when we’re in the loading state and the loaded event happens, we’re now in the ready states but since the ready state has trialed states it’s also going to immediately enter this playing states.
So everything is still going to work just as we expect. We could pause, we could play, we could have maybe other states, but now we have some share transitions between those states. And also these transitions are going to be taken if none of the children specified that transition.
So if for some reason we have a buffering transition on one of the children, then that’s going to be taken as priority over this one. It’s where the same way that in events propagation in the DOM works, we start at the trialed nod. And then if that doesn’t handle it, we propagate it up to the parent’s node and if that doesn’t handle it, we go to the next parent node and so on and so forth.
A compound state is a recursive data structure in XState. And we could specify that, I’ll just go into the visualizer, so we could specify that inside any of the actual states. So, we could give this an initial of one, I’m just going to make some contrived states and then we have states one, two.
And so now if we take a look at this and there’s no implementation for that guard, so I’m gonna get rid of that, we see that we immediately entered the initial state of the loaded state and we could pretty much do this as deep as we want to.
In fact I could just copy this here. And so, now we have even deeper trialed states, so just to show you over here so now we enter the initial state of loaded which I set to two and the initial state of two which I set to one. And you could go as deep as you want with this grouping the states in a way that makes sense for your application.
The general strategy for this by the way is just like you would start with transition actions and move into entry and exit actions, when you see repetition happening, you would do the same with trialed states. So if you see that playing in pause both have transitions in common, you have to stop and think to yourself, how are these states related, if they are and should they be gripped in a parent state instead.
So using compound states is more of an organizational technique which comes with a lot of cool features like we’re gonna talk about in the upcoming lessons such as history states and final states. So it’s definitely a really useful tool that helps you cut down on the number of transitions in your state chart
Compound States Exercise
In this exercise, we want to do pretty much what we did over here and group the playing and the paused states in the ready state. And we’re gonna see why this is useful in future exercises. But right now it’s just to get used to that grouping states into parents states and refactoring so that they’re children of that parent state.
We’re gonna call the parent state the ready state, and we’re gonna specify the initial state, which could be either paused or playing depending on what kind of logic you want to represent. I think at the beginning, we said that the song immediately plays. So if you want the initial state to be playing, then you could go ahead and do that.
Also, another thing to keep in mind too is that when we have transitions between sibling states, like playing and paused, we could specify the target as the key. However, when we have a transition that goes directly from one child state to another potentially unrelated state. I guess you could call this an uncle or an aunt state, because it’s not directly the parent.
Then we have to give this an ID so that we could sort of transcend these barriers and specify the state node from any arbitrary location. And so that’s what we’re doing over here. We add this # and we specify the ID over here. That’s already done for you.
So your only task in this exercise is to group the paused and playing states in a parent’s ready state.
Compound States Solution
Let us implement compound states into our application. So this pause and playing state now need to be children of the ready state. And so we could add the ready state right over here. And then we could move the paused and playing state right into it. So, if I have a states object in here, we could just copy and paste, if I have everything selected, the pause and playing state right into there.
So now this is the overall structure. We have a ready state, and the ready state has two child states, paused and playing. And so we need to specify an initial state, too, so initial, so just have it playing, and there we go. That is all it takes to move things into a parent state.
So now you’re going to see that the application will continue to work, hopefully if I don’t speak too soon, compound states. And so it says invalid transition definition for state node loading. Child state pause does not exist on machine. So let’s debug that. We have loading and it goes to target paused.
So, I forgot, we have to add this to the ready state instead, at least the error message was clear enough. And so now we’re in the ready state, which immediately goes to playing, not paused. And so now we can just click play, paused over here, and yeah. So a couple things to keep in mind, too, is that when we’re doing stuff in, for example, rendering based on what the state value is, there is a tendency to sort of tie this directly to the machine structure.
So, if we want to know whether we’re in the playing or paused state, we might have something like, const isPlaying = states.matches(‘playing’). And so this would have worked prior to the refactor, where we moved everything to a ready state. But now we have to change this to state.matches({ ready ‘playing’ }), because this is the new structure.
And so you might be looking at this and thinking, isn’t that a little bit fragile? And the answer is yes, and that’s why XState has introduced tags as well. So instead of trying to determine the exact structure of the machine in order to match the state against that, we could introduce tags in states.
So I could just call this playing. And down here we could instead, say, state.hastag(‘playing’). Which will return true or false if that tag matches one of the state nodes that are active in the state. And so this is going to work whether we have playing as a parent state, or, sorry, this is gonna work whether we have playing as a top-level state or as a child state of a parent state.
And so it’s pretty refactor-proof, and it’s a good way of abstracting your machine implementation even further. So, one question in the chat, what differences do we get from tags over meta? And so, yeah, I’ll quickly talk about meta, so each state can actually have meta information as well.
For example, in playing, I could have meta, and summary, the song is playing. And then when we console.log this state, Now you’re going to see in the meta, we have a pretty flat structure where we have the full ID of the song, or sorry, of the state, and we have the metadata that we added to that state.
So we have this text that says, the song is playing, under the summary property that we provided. So you can use this instead of tags, but meta is really meant not for identifying which states are active, which is what tags are for, but really for adding any extra metadata to those states.
And this is useful, for example, if you want to generate documentation or have some descriptive parameters for testing, and other things, too. You can even use meta for actually rendering parts of your UI without having to hard code all of it. You could just put it directly in meta.
There’s a lot of use cases for that, but they are conceptually different things. Other than using tags, what are some of the best practices for maintaining deeply nested compound states in large machines? And I would say that the first thing is that you should try not to prematurely add these compound states.
Try to keep your machine as flat as possible until it makes sense to have these compound states. So you could use tags to sort of, at least form a state-level perspective, flatten the structure of the machine so that you only have to read based on the tags. You could also use meta for that same thing.
And you could also target using IDs if you need to cross ancestor boundaries. So if this state needs to target this loading state, then you could do that by using that ID that you provide. But in general, if you see that your machine is getting a little too big, then you could, and we’ll talk about this later, but you could split it up into different actors.
In the same way that you would just split a mega function up into smaller functions. And yeah, someone mentioned in the chat, too, that I guess that’s where actors and nested state machines come in. Yep, that’s absolutely correct.
Parallel States
Parallel States
Another major feature of state charts are parallel states. So when David Harrell invented state charts in 1987, give or take, two of the main features were orthogonality and hierarchy. So the hierarchy comes with compound states where you could have this hierarchy of states, and this orthogonality represents just many states that can be active at the same time.
Now you might be thinking to yourself, wait a minute, finite state machines, I thought that the number one rule was that you can only be in exactly one state at any given time. And that’s still true even with parallel states. So what we’re gonna be modeling over here, I’ll just show you an example in Excalidraw, is we have all of these different states, we have loading and ready.
But let’s say that we also want to control the volume in the same state machine. So, Whether it’s loading or playing or paused or in this ready state, it doesn’t matter. The volume can still be muted and unmuted at any time. It sort of lives independently from all of these states.
So what we would have to do, if we were trying to represent this strictly as a state machine, is we would have to have, and let me just change this color real quick. We would have to have a loadingMuted state. And a loadingUnmuted state, I’m gonna spell that right, unmuted.
And we would have to have all of the states like this too. We would have to have playingMuted, playingUnmuted. PauseMuted, pausedUnmuted. And so now you see that our three states have multiplied into six different states. And so now if we have to represent something else such as whether the media player is expanded or collapsed, now you’re multiplying this by two again.
And then if you want to represent different states, such as if you if you decide to represent something being liked, unliked, or disliked. Now you’re multiplying all those states by three, and this is where state explosion happens. So that’s why with both compounds state and parallel state, we can really manage and prevent this state explosion from happening.
So the way we do this is instead of creating a combination of all those states, like a Mexican or a Chinese restaurants where it’s just like a menu full of 100 different meal options, which are really just combinations of the same eight ingredients. We’re going to represent these as parallel states.
So, the way we do that is by creating what are called regions, and don’t be scared by the term regions, it’s just a way of saying, just child parallel state. So let’s say that we have this entire machine as a parallel state, and we have two separate regions, and again think of regions as states, we have this which represents the media player.
And then we have a separate region, which represents the volume. And so, this volume can be muted, it’s really small, or it could be unmuted. And so both of these states can transition between each other. We could have muted going to unmuted, or unmuted going to muted, so just like this.
And these are just on different events, either the mute, the unmute, or we could have a volume.toggle event as well. Ad so in xstate, the way that you represent these parallel states is by specifying the state node as type of parallel. So let’s go back to our example machine here and just create a little example.
So instead of providing an initial state, I’m just going to provide type parallel on the machine itself. Now we’re gonna specify our states as normal, but these states are going to be parallel states. So we could have, I’m gonna have something for, let’s say mode, and so our state can be dark mode, or light mode.
And initially, we’re gonna do light mode, and then we might have another parallel state for display. So initial on, states on, off. And so now you see that this is what these parallel states look like. We’re both in the display on state, and the mode light state. Now we could go back and forth between the mode light and dark state without affecting the display, or we could go back and forth between the on and off states in display without affecting the mode.
Or the same event can be handled by states inside this parallel region, and this parallel region, so you might have multiple transitions happening at the same time. Again, just like compound states, parallel states are an organizational technique for reducing the number of states and transitions in your state machine.
Parallel States Exercise
So now we’re gonna be doing an exercise adding parallel states into our machine. Just like we showed in the diagram right over here, we are gonna be splitting up our entire media state machine into two separate regions. We have the player region and the volume region. And so all of our loading and ready states, those are going to go inside the player region.
And then these two states, unmuted and muted, are going to go inside the volume region. So make sure that inside your volume region you’re giving it in initial state and you could have it be unmuted because I mean you want to listen to the song. So unmuted makes sense, and you want to move this initial loading transition into that new player region.
Go ahead and refactor this day chart. So it has those two player in volume parallel regions. And let’s see how it goes.
Parallel States Solution
All right, let’s talk about adding parallel states into our application. Again, we want it to look like this where we have player and volume. All right, so the first thing to do is to actually have those two states as the child states of this machine, which this machine itself is going to be the parallel state.
So we have to first specify type parallel and then our states. And I know things are being duplicated right now, but that’s what happens while you’re refactoring. So our states are going to be ready, or sorry, not ready. Our states are going to be player and volume. And so now we can move all of these states over here, well, not all of them.
But we can move this initial into there and then we’re going to be copying the loading and ready states over here into states. So let’s just close that up and now we’re gonna be copying these two states into volume. The initial state is going to be unmuted, and so now we have our states of unmuted and muted.
Now we have an extra thing over there. All right, so now let’s test our application and see what we have, six parallel states. Okay, so if everything works, now we should have a volume that mutes and unmutes. So we know that that region is working, and so we also have our play and pause working as well.
So we know that these are working. Now if you want to do something extra, you could also scope these events to their appropriate reasons. For example, the skip, like, unlike, dislike, and audio.time, these are transitions that only have actions, except for this target one which is targeting an ID so we can move it anywhere anyway.
We can move those to here. And so now we have everything nice and organized inside of this player state. And we could see, okay, the player region is responsible for these skip, like, unlike, dislike, and audio.time events. And so, we could do the same thing over here for the volume controls and just put it on there.
And so, now we don’t need this anymore and everything should be working the same.
Parallel States Q&A
So, one of the questions might be why would we do that? And honestly, it’s only an organizational thing and keeping things nice and organized. Just so that you could clearly see, okay, these are the responsibility of this player state. And these transitions are the responsibility of this volume state, anything that happens in there?
All right, one question asked in the chat. Is it better to use final in nested states and on exit?or on done rather than target in ID? And the answer is yes. And we’re gonna be taking a look at that at the next section, or one question asked in the chat.
Was in order to make it a bit more readable is it recommended to split the child machines or the child states into their own functions or files? And honestly, this is up to you. Think about how you would structure your own code base. Since this is just an object, you could separate these outs into separate variables and just call it volume state or something like that.
And then import it into this file and so that might make things a little bit more clear. Also if you can’t do that then you could just basically collapse everything and get a really nice higher level overview of how. Just everything that can happen. And so while yes, this is big, try to imagine this in code without a state machine or a state charts.
Over here. I could take a glance at everything and understand what the different states of my app can be in such as loading or ready. And what can happen in each of those states. And so I also see that I’m handling the player in the volume if I also handle something else over here.
Then I have a really nice higher level overview of those things. And I also have a way of saying or of seeing what transitions in events are handled in the state machine. So I already have a really good overview of all the features of my application just by looking at this object.
And if your object is big, that means that you have a lot of features and you have a lot of different ways that users can interact with the application. Which definitely not a bad thing but yes having that organized is something that’s up to you and something that of course is always beneficial to do.
So yeah I do recommend if you want to split it up into separate files that’s up to you and in a future lesson. We’re gonna talk bout just the idea of splitting things up into separate machines as well if you wanna do it that way. And that’s essentially the same idea as splitting it up into separate files the follow up question is will you be able to get a Serializable machine.
If we’re exporting this from a different file into this machine and the answer is yes. And that’s because in our player machine we are collecting all of those and we are basically creating a runtime big objects value. Of all of the different states and transitions regardless of where they live in our code base.
And that is something that, for example, if you’re using X state inspect. That’s something that is sent over the wire to the inspector, and it’s still going to be able to see the entire machine. So you know what, I’ll just do a Small little example over here. We have an object over here that’s not really reading anything that we need to import.
So I’m just gonna copy and paste this and call this volume state. And we’re gonna put it over here so volumes state equals that big objects. And you can imagine that this might live in a separate file. But now, we see that we have volume as volume state, and this is going to be visualized just the same.
So we could even, for example, copy all of this and put it in our visualizer. And the raise of n is missing, so I’m just going to comment that out just for the sake of time. And so you’re going to see that even though we have that as a separate parts it’s still being visualized the same.
So we have the volume right down here. [LAUGH] And that’s something else that we also added in there too and also the player. We have loading ready with our playing in paused states and so it could go between those states. And we could see just the different regions that we have in our application.
Final States
Final States
When we talk about the mathematical definition of Stephen scenes, there is five part to it. There is an initial state, a finite set of state, a finite set of transitions, a finite set of events and a final state. Or final states there could be multiple final states but a final state represents somewhere where the machine ends.
So if you have a machine that’s doing a certain process where it has a clearly defined start In an end, then the final state represents that process being finished. Whether it errored out or it is successful and it represents the fact that that machine can no longer make any further transitions because it is in that final state.
So, final states are used to signify that something is done. And in the machine a final state can be used to,- like I said signify the end of a process, but we could also include final states inside of compound and parallel states. So, let me just give you an example here.
Now our big example that we have over here, am gonna remove a few transition just to make things a little bit clear. Let’s say the we are on the loading state and we have a success state that is going to go to loaded and am also going to remove this axle just to make things a little bit more clear.
We could mark this as type, final. And so, Let’s see what happens when we do that. All right, so we’re loading and when we send for example, let’s send loaded events. Success events, just kidding. Getting my machines confused. Now we’re in loaded. And the states also has a done property that says done is true.
And so this is a good way to just determine whether a machine is done or not. And so you see over here this previous state has a done false property. [COUGH] Now like I said, Done states or sorry, final states are also useful for signifying that part of the machine is done.
And then you could handle what happens when that part of a machine such as a compound, or parallel state is done. So, for instance, when we’re in this loading state, let’s see that we have to do a few things. I’m gonna make this a compound state getting data and then states getting data.
[COUGH] And I could add a transition here but I’m being lazy I’m gonna add an after transition, which is just a transition that happens after a certain amount of time. And yes, you can think of time as an event as well. So I’m gonna do this after a second.
I’m gonna be getting more data. So getting more data. And then after let’s make this 500. We have a finished state. So this finish state, we’re gonna mark as type final. Now keep in mind that this is not a top level state of the machine, this is. So we have this top level loaded state which is final, but this is a final state inside of this loading state.
So what happens over here? First let’s log this and see what happens. It says getting data then getting more data then finished, okay? So what uses that? Well, when we’re in the compound state, we could listen to a state being done by using on done over here, and so on.
Done, I’m just gonna say, actions console.log done just to show you that that gets triggered. So, we reached the finish state, and we have the done message over there. Now, this on done transition is just as implied it is a transition so we could transition, to the loaded state.
And now you’re gonna see that it’s going to immediately go to that loaded state because once we reach the final state of finished, it’s going to take this transition and go to the loaded state. Now you can also have final states in parallel states as well. And we talked about top level final states of the machine.
But with parallel states, it’s a little bit different and I really recommend you read the documentation on this because with parallel states the on done, is called when all of the child states reach their respective final states. So for example if I have oops, if I have this state over here and maybe another state here and so yeah let’s say I have all of these.
And let’s say that this date is a final state which I’m going to mark with a double box over here. And let’s say that we have one two over here. And let’s say this is an on done transition on that parallel states. So what happens here is when we reach the final state of one of the regions, so let me just mark this as green, then this on done is not going to be called just yet.
But when we reach the final states of all of the regions in parallel states, then and only then is the onDone transition taken in that parallel state. Now, this is more of a niche use case but it is useful to know. And this is something that is defined in SCXML in the original state charts paper.
And it could be useful in certain situations but most of the time you’re going to be using final states in the context of compound states or in the machine overall.
Final States Exercise
Now let’s go to an exercise on final states. Something that was mentioned in the chat was the facts that we have this sort of ugly looking transition over here, because we’re targeting an ID. So it’s ugly because we’re in a child state of a parent state, but we want this to go directly to an unrelated parent state.
Like I called it, it’s sort of like an uncle or sibling states. But we could represent this more semantically and in a cleaner way, by using final states instead. So what I want you to do is change this target to go to a finished state, make this finished state a final state and observe what happens with this on done transition.
And test it in your app and make sure that everything still works the same.
Final States Solution
All right, so let’s add final states into our application just to clean things up a little bit, since we are targeting this loading state with an ID, and we want to avoid doing that just to model things a little bit more cleanly. So first things first, instead of going to loading, let’s make a finished state right over here, and let’s target that state instead.
So what’s happening over here is when the elapsed is greater than the duration, now we’re saying okay, this song is now gonna be finished. And by doing that we are now just separating from the fact that we have this loading state. So now we don’t even need to know about this loading state because it becomes the responsibility of the player states.
So if we want to change this loading state and maybe just, we have a song already cached or we want to replay the song if we’re on repeat and we don’t need to load it again. Then none of those details matter because all we’re saying is, hey, parent state, I’m finished.
And so we specify that with type: [COUGH] ‘final’. All right, so now we are going to make sure that everything still works. And we’re going to go to 07-final-states. [COUGH] So now we see that play and pause still work. But we do want to trigger this when elapsed is greater than or equal to duration.
And again, the way that this happens is using the AUDIO.TIME event. So, if we do service.send, type: ‘AUDIO.TIME’, and we give it a currentTime of something big, like 1,000. What should happen is it should go back to the loading state, which it does. And of course we could go from loading to loaded.
I’m just gonna copy this, and see that it goes back there. So our final state is working.
Final States Q&A
In the example we showed the actor transition and someone asked after is he kind of declarative sets timeout then, yeah that’s absolutely correct so after transition causes an action that the interpreter takes. And is going to execute it by moving it into a set timeout and after a certain timeout it’s going to send an event back to the machine.
And so that’s how that after transition sugar works. Can the state machine be restarted from the on done Handler, by doing a race events? So the answer is no. Once a state machine has reached its top level final State, then it is done. It can’t be restarted. The way that you would restart the machine is just by recreating it.
Each region still has its own on done events, which would be called when that region reaches its final states. Correct? So this is referring to the parallel states over here. So the way that this actually works. And so just to make this a little bit more clear, let’s instead of giving these regions a thought like this, we are going to give it this.
So you can imagine each of these regions looking like this and this is also how it’s represented in the X state visualizer. Just to to make things a little bit more clear, each of these regions can have its own on done transition, in a parallel state they typically don’t or at least I haven’t seen it that way.
But yes, they can have its own on transition. Now this on transition for this parents elements is actually relative to these children. This on done is actually on the grandparents Ellen. And so this is actually in the SCXML algorithm so if I grandparents, there we go an this is actually interesting to look at it says if the grandparents is a parallel states state.
And the child states of that grandparents if every one of them so every one of those compound states is in a final state, then we on cue this done.states. And whatever events in an X state we provide sugar for that via the on done transition so that’s sort of a long answer but yeah it’s handled in the grandparents for parallel states.
And so if you envision it by like when this is done and this is done that means that these two are also done which I’ll signify in green then the grandparents is going to say okay. Both of these compound states are in their final states so I, I am actually done as well.
Is the loading in the on done transition needed because we are transitioning to a child state. So, let’s take a look at that, loading, first, let’s find it. All right, so on done target loading. All right, so the reason that this is, is because first of all we’re defining this transition on the parent states so this player state over here.
And this target is basically saying that this parent state needs to move itself to the loading state so if we go back to scale draw over here we’re basically saying that this player state. And we can represent it like this, make sure we have the right line. Yep.
Okay. We don’t, hold on. There we go. Okay. So, we can represent it by saying okay when this player state is done, it is going to transition to its own child states. So this is on done. In this is because we also have that we have ready and we have the finish dates just add the states we have the finish state over here in that always transition.
[COUGH] And because this is a final state, that is what’s gonna cause the on Dunn transition to be taken up here and transition itself back into the loading state. So that’s why you have that dot notation because this one when you represent a state key as they target, you’re representing it’s sibling.
And so that is not what the most natural approaches. So if you are in the loading stage, we are going to wanna go to a sibling stage, which is ready. So that is why there is no dots there. But if you want to go a child of the state, then you are going to use a dot in order to signify it to go inside of that state.
History States
History States
All right, let’s talk about another powerful feature of state charts, and that’s history states. What exactly are history states and what are they used for? History states are not exactly the same as previous states, because a previous state just represents going arbitrarily back in time and state machines need to be deterministic.
So if you do wanna go back to some other state, there needs to be an explicit transition from your current states to whatever previous state. So it sort of needs to be hard coded. History states are actually used in conjunction with compound states for revisiting those states with the previous child states that the compound state was in.
So let me just describe this in a diagram. Let’s say that we have a compound state over here and this compound state has two child states. So we have A, And we have B. So let’s also say that this compound state has a sibling state, and we’ll just put this over here, and it could transition back and forth between the sibling state.
So we have an arrow going there, and an arrow or transition, whatever you want to call it going there, of course we need an initial state. So we’re just going to go into A and pretend that there’s like a way to transition from A to B. So we have A and B right here.
All right, so let’s say that we answer this machine and we’re in the state A and then a transition is taken out of this compound state that goes to this other state. And then a transition from the other states goes back to that compound states. Well, A is going to be revisited again and that’s going to be the next states and that’s because it is the initial state.
However, let’s say that we transition from A to B. And now B is our current states. So now when we go to other, we’re in the other state, so I’m just gonna color this something else. And now when we transition back, I’m gonna color this purple, what do you think will happen?
Well, since we’re entering this compound states, we are going to also immediately enter the initial states and not the most recent child state of the states. So this might not be what we expect or what we wants to model. And so that’s where history states come in handy.
We could have this special, it’s called a pseudo states. It’s defined the same as any other state, but it’s a state that serves a special purpose. And so we’re just gonna mark it with an H over here. And this is called a history state. So instead of going to this parent state from this other state, we go from the other states to a history state.
And this history state is a child of this parent state, which means it’s also a sibling of the A and B states. So what’s gonna happen now is if we’re in the B state over here, and we transition to the other state. Now, when we transition back, and keep in mind we’re transitioning directly into the history state.
That is actually going to say, all right, we want to really transition to the most recent child state of this parent state, which is going to be B. And so that’s why history states are best for remembering what the most recently visited child state in this parent state was and going back to it.
So it’s not a mechanism for going back in states. There’s completely different methods of doing that. And you would also typically do that outside of the state machine itself. But rather, it’s a way for you to declaratively represent, I want to go back to the most recently visited child state in this parents.
So there’s also such thing as deep history. And so let’s say that this B state also has some child states of its own and let’s say that this is also a compound state. So now instead of having to have a history state here and also have history state here and if we have deeper levels adding more and more history states, we could just represent this as deep history.
And in state chart notation that’s represented with H*. And so what this deep history state is gonna do is it’s going to go to the most recently visited child. But it’s also going to go to that child’s most recently visited children as well. So it’s an infinitely deep, or a recursive if you will, history state.
And so that could be useful in many situations.
History States Exercise
Let’s go to our next exercise, which is on history states. Currently, there is a problem with our application. Not sure if you notice, but right now in our application, if we press next, and then we load the song. Everything seems to be working. Now let’s say right now this player is currently in the playing states, or really it’s in the ready playing state.
Let’s say that we pause this. And now let’s say that we skip to the next song. So now we’re loading and we loaded the song. Now the next song is going to immediately start playing. This can be undesired behavior because previously, we pause the song, and then we skipped.
So what a user would expect to happen would be that the player should still be in the paused state, because that’s what we were previously on. And we pause the song for a reason. So, what we’re going to do is we’re going to model this in the state chart, right now, when the loaded event happens, we go directly to the ready states and the ready state.
The initial state is always playing right now, but what we want to do is instead we want to visit the most recently active state in this ready compound state. So that if we were on the pause state when we revisit this ready State, we want to go back to the paused State instead of going to the playing state by default.
So add a sibling history state here. And, yeah, go ahead and implement it, by the way, the way that history states are defined in X state. So, let me. Just go here real quick. Is that when you have a compound state, so if we have initial test, states test, and you would specify the history state by naming it wherever you want.
Typically I like to name it hist and then you specify a type of history. And then by default the history seat is going to be a shallow history state but if you want it to be a deep history state, you give it a history value of deep. But this is not a common use case, so typically all you need to do is specify type as history.
History States Exercise
Let’s go to our next exercise, which is on history states. Currently, there is a problem with our application. Not sure if you notice, but right now in our application, if we press next, and then we load the song. Everything seems to be working. Now let’s say right now this player is currently in the playing states, or really it’s in the ready playing state.
Let’s say that we pause this. And now let’s say that we skip to the next song. So now we’re loading and we loaded the song. Now the next song is going to immediately start playing. This can be undesired behavior because previously, we pause the song, and then we skipped.
So what a user would expect to happen would be that the player should still be in the paused state, because that’s what we were previously on. And we pause the song for a reason. So, what we’re going to do is we’re going to model this in the state chart, right now, when the loaded event happens, we go directly to the ready states and the ready state.
The initial state is always playing right now, but what we want to do is instead we want to visit the most recently active state in this ready compound state. So that if we were on the pause state when we revisit this ready State, we want to go back to the paused State instead of going to the playing state by default.
So add a sibling history state here. And, yeah, go ahead and implement it, by the way, the way that history states are defined in X state. So, let me. Just go here real quick. Is that when you have a compound state, so if we have initial test, states test, and you would specify the history state by naming it wherever you want.
Typically I like to name it hist and then you specify a type of history. And then by default the history seat is going to be a shallow history state but if you want it to be a deep history state, you give it a history value of deep. But this is not a common use case, so typically all you need to do is specify type as history.
History States Solution
Over here instead of targeting the ready states, we want to target a history state instead so that we can remember what child state of the ready state we were already on. So we currently have the pausing playing state. And now we just need to add a history state here which I’m just gonna call hist.
And I’m gonna give it a type of history. And we’re going to target that ready hist states, which is represented here by .chaining we have ready .hist so we could target this child state right here. So now just by doing that when we’re playing and we skip to the next song.
We’re still playing, and so now if we’re paused and we skip to the next song, now we’re paused again, so we see that our history states still works.
History States Q&A
Somebody asks, so this is like when we want async work going on and then coming back to a machine? And yes, this is exactly how you could think about it, and this is what we’re doing exactly here in this exercise. We have this async work of loading a song, and then once that’s done, and we come back to our ready states, we remember what the previous, or not the previous, but we remember what the child state that that ready state was in.
Is there a way to set some default data when we trigger an action by clicking events in the inspector to avoid typing the events data each time? So this refers to the visualizer and in the visualizer, you can send in events with payload over here. So I could send some events and I could add some payload, Hello, and then I could go ahead and send that events.
Right now, there is no way to have defaults data, but that is coming as a future feature inside the visualizer. Does history work for parallel states? So each region returns to its previously selected state? And the answer is yes, because each region is most of the time, unless you don’t have any child states, every region is a compound state.
So you can specify a region that has child states and also has a sibling history state that you could go into. What makes an infinite or a recursive machine? So, we have finite state machines, which the finite means the finite number of states events and transitions. An infinite state machine doesn’t really make sense.
So you could think of an infinite state machine as a finite state machine that just can have infinite data, and that actually sort of is already like these state machines and state charts that have context. So you might imagine, for example, a machine that has a count value that could count all the way up to infinity.
Then if you represent each one of the aggregate states as including the count, then, of course, this state machine has an infinite number of states. However, if you just consider the behaviors, then the behaviors should be finite. And even though you might have an infinite amount of combinations of data, which is definitely the case for most applications, the behaviors themselves are finite.
So that’s what the finite means in finite state machines.
Actor Model
Actor Model Overview
Now we’re going to get into a big topic. And this is something that XD is really built around because it’s just such a useful concept. And it’s also something that is going to be simpler than you might think. And that’s the actor model. So the actor model is a model of computation where basically everything is an actor.
So what do we mean by an actor? And again, I’m going to show you just by drawing. So let’s pretend that this circle is an actor. And we’ll even label it as Actor. So an actor can do a few things. First of all, the actor has a behavior.
And so, this behavior determines what it does, depending on which events it receives. So, we could have this behavior, or we could have a different behavior. So, if we’re considering this to be a human actor, just like the example I gave earlier, we could have an Asleep behavior and an Awake behavior.
And we could transition between these two behaviors. So, event might come in, and I’m going to label this as just an arrow that comes into the actor. And this event can just be something like ALARM. And so, this ALARM can cause the actor to change its behavior from Asleep to Awake.
So actors can actually have this internal state machine. Or in other words, their behavior can be represented by many things. But one of the best things hopefully you learn from this workshop is that it’s usually best to describe behaviors in terms of state machines. And so this actor can change its behavior depending on an event coming in.
And like we learned, this changing of behavior due to an event can include things like updating its context or performing some side effects. So what actors can also do in response to an event is the actors themselves can send events to other actors. So I’m just gonna label this as AN_EVENT.
And we are gonna create another actor over here with the right stroke, so Another Actor. Now, actors can only send events to actors that they have reference to. So this actor has a reference to another actor, so it could send it an event. However, let’s say that we have a third actor right over here.
Since this actor has no way of knowing this, I’m just gonna call this Actor 3, has no way of knowing that this Actor 3 exists, it’s unable to send it an event. Now what actors could also do is they can spawn other actors and basically create new actors.
So I’m gonna represent this by just having a line over here where this is an actor spawned by this other actor. And of course, since this actor spawned that actor, it can send events to it because it has a reference to it. Now depending on your implementation of actors, actors can also send events back to the parent’s actor because it’s like the they’re born with that information.
They know who their parent is so they can send events back and forth. One of the easiest ways to conceptualize the actor model is pretending that each actor is a human just like you and me. I’m talking to you and you could talk to me, as long as you have contact with me.
For example, if you have my phone number or email address or Twitter username, you have a reference to me. So you can send messages to me. And you can also include, for example, your email, and I can reply to you via email because now I have reference to you as well.
Another concept of actors is actors each have their own local state. And this state cannot be directly read by other actors. So everything is done via sending events. And so with actors, if one actor wants to read the state of another actor, what typically happens, and this is best represented in HTTP requests.
Is that this actor would send a request like, I want to get this part of data from you. And after some async operation, this actor might respond and say, okay, here’s the information. Now you could also model publishing and subscribing as implicit events. So you could say that this actor can subscribe to some part of this other actor.
Which in reality is this actor sending events to its subscribers on some sort of regular interval or whenever something changes. So, in short, the actor model is representing each entity in your application as something that you could send events to. Something that could send events to other actors it has reference to, and something that can also spawn its own actors.
And an actor also has its own internal state which can change depending on what events it was sent.
Actor Model in Vanilla JavaScript
Let’s actually create an actor and we’re gonna be starting from scratch here not in this file but in main.js. So, I’m gonna to be deleting all of this, all right. So let’s create a very simple actor. And echoing the the first exercise that we did, we’re gonna be doing this without using any libraries.
So we’re gonna push XD to the side a bit just so that we understand the actor model a little bit better. So we’re gonna have function createActor and let’s give it a behavior as well. So this behavior can be represented by a function just like the reducer function that we talked about in the beginning.
So I’m just gonna call this function countBehavior and that takes the current states in an event and let’s just make this really simple. So if event.type = INC, so if we’re incrementing then we return state where the count is state.count + 1, all pretty simple stuff, return the state.
So, in createActor what we ultimately want to do is we want to return an object where, just like we described in this diagram over here, we could send an event to the actor and that should do something. Now, this send is a void function, which means it doesn’t return anything.
It just receives the event and then it could change its own behavior with the event but it’s not gonna be synchronous or immediately respond to the sender. And so let’s also have an initial state over here. So we’re gonna call it currentState = behavior. Let’s also have an initial state here.
So we’re going to just have the current state be this initial state. So whenever we’re sent an event, we’re going to update the current state. So currentState = behavior(currentState and the event that we received. Now this of course looks very familiar to the first exercise we’ve done. So, in a way we’re coming full circle over here.
So now let’s create this actor so const actor = createActor, and we’re gonna create it with our countsBehavior, which, again, is just a reducer, so countBehavior. And we’re gonna give it an initial state of count of just 42, just to make sure that things are working and window.actor = actor.
Okay, so now we have our actor and we see that this actor, it definitely has its own local state. And we know this cuz, well, we don’t know this, because it’s internal, it’s local, we can’t see it, but we can send it events. So, just to make sure that things are happening and get a little bit of observability in here, I’m gonna console.log the current state.
And now we could see that if we send the actor an event, so type INC, It’s going to log 43. Remember, we started on 42 and so now it’s on 43. Now we’re gonna send it INC again, 44, 45, 46 and it keeps going up and up. So that’s the actor changing its own internal states.
1 | function countBehavior(state, event) { |
Now its internal state can include other actors that it decides to create. So all right, we have send. But to make things a little bit more practical for real world usage we also want some way to subscribe to the actor’s current state, if the actor so chooses to share that state with us.
So, let’s create, I’m just gonna call this listeners, and this is going to be a set. And so we’re gonna subscribe to these. So subscrib Listener and listeners.add that listener and we’re also gonna call the listener with the current state. So every time the state changes here, now we’re gonna have some way of notifying or emitting the current state to all of our listeners.
So listeners.forEach listener, we’re gonna call listener with the new currentState. Okay, so let’s try this. If we do actor.subscribe now, and we say whatever the value is, we’re gonna console.Log the value. And now if we send, it’s going to log the value all the same. Remember, we got rid of our console.log in here and now we’re doing it by using that subscriber.
1 | function countBehavior(state, event) { |
So you could think of this subscriber as an implicit way of sending events. And so what we could sort of conceptually do with actors too is have some way of saying, okay, let’s pretend that I’m already subscribed to you but I’m not going to be listening. Or I don’t want to receive an event for every single time you update.
I just sorta want you to cache that away. And so I can easily access that value that you emitted to me, just whenever I want. And so that’s what we could do with, if we have some sorta getSnapshot function, we could just return the currentState. All right, so now if we send the actor a bunch of stuff and we say actor.getSnapshot now we have counts 51.
1 |
|
Because remember, this is in implicit subscription, we’re not directly reading the actor’s internal state. We’re just getting the last known value or the last known snapshot of that actor. And so, this in short, the reason why I created this from scratch is becasue I wanted to show you just how things are working internally with X State and X State’s version of the actor model.
So the core the functionality of an actor is being able to send it an event. These other two things don’t directly break the actor model, but they just make it a lot more practical for real world usage. So subscribe and getSnapshot, you can think of these as abstractions on top of the actor model.
Ways to Invoke an Actor
Okay, so now let’s talk about using the actor model within a machine. So I’m gonna make a new machine here, createMachine. And we’re not gonna have any states in this machine just because I wanna show you just the basic features of invoking an actor. So with invoke, the machine itself when it’s interpreted, it becomes an actor.
And so we know this because we’ve been using state machines in this way where we interpret them, we subscribe to them, we can send stuff to them. And this is a pretty useful interface that covers a lot of use cases. So the other part of the actor model is being able to spawn or create your own actors and so we could do that in next state using invoke.
So one of the simplest actors to invoke is a Promise. And so everything that goes in this source by the way for the invoke, is a way of creating that actor. So just like many other functions in XD it takes the context in the event, and it should return the thing that can be turned into an actor.
So for instance, we’re going to be returning a Promise. And so I’m just going to say, this is gonna be a new Promise that resolves after some time-out. So resolve with 42 and we’re gonna have this resolved after a second. Okay, so now let’s interpret this machine, And, Let’s also subscribe to it.
So service.subscribe(state, console.log(state, just to see exactly what happens. All right, so we reload after a second, you see that something happens over here and we get an event pack. We get this done.invoke dot whatever this is. So to make this a little bit nicer, you can also give the invoking ID.
So id, we’re just gonna be calling this fetchNumber. And so now after one second, this event has a nicer name. It’s done.invoke.fetchNumber. And the data that is returned from that Promise or a result from it is in this data property of the events. And so what we could do is have an onDone transition inside of this invoke.
So onDone, actions and we are going to, let’s just log the event. So I’m gonna say console.log(‘DONE!’) and we’re gonna get the events. All right, cool, so we have done and here is the events, we have done.invoke.fetchNumber with data of 42. And so just like the other onDone transition, this can target a different state.
So if we have initial loading, states, loading, we can put this invoke inside of that state and onDone, we could go to success, And we could target success over here, so target success. And it’s still going to log(‘DONE, so I wanna log the state.value now. And so now let’s see what happens.
So we’re in the loading state. And after a second we get that same done.invoke.fetchNumber events, but it calls the onDone transition, and it targets the success date. So now, we’re in success. One thing that is in SEX mail, which I find to be a really useful feature. And I’m gonna bump this up to five seconds, just so we could demonstrate is the facts that when we leave this loading states, this invocation will actually be cancelled.
So whatever this Promise resolves with, it’s going to be ignored as we expect because this invocation is only active while the state machine is in the states. So let’s now add a transition, so on CANCEL. We’re gonna go to the cancelled, so we’re just leaving the state, we’re not doing any specific cancellation.
We’re not dealing with this invoke, we just know that it’s gonna be cancelled when we leave that states, so window.service = service. I have five seconds to type this, so let me jump this up to 10 seconds. All right, so service.send( ‘CANCEL, Wow, I think it’s gonna. Okay there we go, we’re canceled, somehow I made that below 10 seconds but yeah, we do go to the canceled state and now.
The Promise is not, so we don’t get that done.invoke dot that data because it was cancelled. So just to verify that this is working. All right, I sense cancel, but it was too fast for me, so I’m gonna do this again, and I’m gonna console.log(‘ resolved, There we go.
1 | import './style.css'; |
So we have resolved, which means the Promise did resolve, but it did not cause any transition. This onDone transition wasn’t taken, we don’t have this done events. And that’s because we’re in a state where this transition doesn’t do anything. So you get cancellation for free basically. And this is sort of going back to the idea that state machines prevent impossible states and impossible transitions.
Because when we’re not in the loading state, we should not be handling this onDone of this invoke. You could also have on error if you want, and that would be a good exercise to you at home, just learning how to handle errors in invocations as well. But yeah.
So another thing that you can invoke is actually, a lot more flexible than a Promise because a Promise cannot receive any more events. It started and then it’s gonna go do something it think, and then it might eventually return with the value. Now, we need something a little bit more powerful than that and so that’s what invoking callbacks is for.
So invokes callback, I’m just gonna put it up here. It has a special function signature where it takes two arguments. It takes send back arguments where we basically can send an event back to the parents and it takes an on receive. I’m just gonna call this receive arguments as well.
And so both of these are really useful for doing communication back and forth between the machine. So I’m gonna say receive and that’s gonna take an event and so we could console.log(‘ Received in that events. And so let’s actually just try this out for now. Instead of this, I’m going to be replacing this with that callback.
And so we have fetchNumber, and this id is gonna be important for communicating back to that invoked actor. So instead of canceling, I’m gonna have a on notify events, And that’s going to, gonna grab send from here, that’s going to do a send action. And we’re going to send what did we call it, no, let’s find out over here actually, yeah, we didn’t specify an event, so we could just send any events.
So ANY_EVENT, and we’re gonna send it to that ID. So fetchNumber. All right, so now when we reload and I’m gonna send the NOTIFY events. It says received ANY_EVENT from the machine. So there’s a lot of useful actions like this. This is ANY_EVENT, but if I wants to just straight-up forward that event, I could use the forward action, which I think is in here, might be, let’s find out.
So forward, and then you could yeah, just specify it that way but that’s something that you could discover in the documentation. This is something that we’re gonna be using in the actual exercise itself. Okay, so with the callback, you could receive an events and do something based on that event, and you could also send something back.
So let’s say that whenever I receive an event, I’m gonna setTimeout And I’m gonna send back just type PING, I guess. And we’re gonna do that after a second, whenever I receive an event, so let’s try that out. So here’s how we’re gonna model it. When we notify, we’re gonna send ANY_EVENT to that, that’s number actor.
After a second, that actor is going to send a PING event, and we’re gonna respond to it. Or we’re going to respond to the events in the machine by just transitioning to the success state. So it seems a little bit complicated, but we’re gonna make it a little bit more clear in a minute.
All right, so let’s try this out. We’re in the loading state, we send notify, and then after a second, we end up in the success states. Okay, so what exactly is happening over here? I know we just talked about it, but just like using state machines and state charts, it is nice to see them diagram form.
And so that’s why I like swimlanes.io as a tool for doing this. So okay, we have this machine over here. And this is fetch number, so it’s sending ANY_EVENT to fetchNumber. So after one second, fetchNumber is sending the machine a PING events. So this sort of describes the communication between these two different actors.
1 | import './style.css'; |
When the machine, sorry, when the I’m just gonna call this the client. When the client sends the machine NOTIFY, that’s gonna cause the machine to send ANY_EVENT to fetchNumber, fetchNumber is going to send PING back to the machine. And some of the machine is going to transition to the success state.
So this is called a sequence diagram, and it’s really useful for diagramming the communication between actors. And so I recommend you use swimlanes.io or other tools for sequence diagrams in your application. Just to describe the different use cases of how different actors can communicate with each other. They’re really useful tools.
All right, so the last thing that I want to show you that you can invoke, and we’re gonna be using a machine instead of a callback here. I’m just gonna call this const fetchMachine, and I’m gonna be creating a new machine over here. So when I enter this machine, let’s say that the initial state is fetching.
And just to make things simple, after one second, we’re gonna go to the success states. And this success state is gonna have type of final. All right, so in this machine, now we have a final state as a top level state. And so this signifies that the machine is done.
Now, just like you remember from invoking a Promise, we could think of this invoking of a machine in the same way. Where this machine is like a combination of invoking a promise in a callback because the machine can be done. The machine can also send events to the parents machine and the machine can also receive events from the machine as well.
So it is really powerful. And we could also, and this is something that we haven’t exactly talked about with final states, but we could also send its data. So we could send it counts 42, which is the example data that I’d love to send all the time. And now instead of this context events callback, we could just provide the fetch machine itself.
All right, so now let’s see what happens. We’re no longer, we don’t have the NOTIFY or PING, it’s going to invoke the machine, and after one second, this machine is going to be in its final state. And so it’s going to send this parent machine, a done event, and we’re going to see, hopefully, this done data logged in here.
So when we go back here, we see it says done, if we look at the data, we have counts as 42 and we transition to the success date. So that’s showing you how machines can also invoke other machines too. And so like I said, we could also send events to that machine and everything’s going to work just as expected.
1 | import './style.css'; |
One final thing before we get off this topic, with a callback, it is important that in this callback, you might be doing some imperative stuff. Or some leaky stuff such as having a setTimeout or set interval or maybe you open a web socket connection. There’s literally anything you could do in this callback.
But there needs to be a way to dispose of the callback as well. And so that’s why you could return a disposal function. So if I just say let timeout and just being a little bit responsible over here timeout = setTimeout. Then I could I think it’s called clearTimeout, I always get confused whether it’s clear or cancel, clearTimeouts that’s timeout.
So this is just being a good citizen preventing memory leaks, just showing you that you can return that cleanup function. All right, so the other thing that is something that I do recommend you research is spawning an actor. And so spawning is basically the same as invoking, but the spawn actor does not live in the state.
In fact, it lives for the entire lifetime of the machine. And if you want to stop it, it must be manually stopped. And so that’s basically the only difference between spawning an actor and invoking an actor. So I recommend if you want to look up the use cases for spawning in actor, read the documentation on it.
There are a few examples on that as well. But we won’t be going over that in too much detail in this course. We’re just gonna be talking about invoking actors for the sake of this example.
Actor Model Exercise
Let’s go to our final exercise and this exercise is on actors. So we have two types of actors that we talked about, or rather behaviors which is this callback behavior over here creates fake audio. And this is something that you don’t really need to know too much about the implementation details.
But this createFakeAudio is gonna create something that feels just like the audio interface that you get with the Web Audio API. So this is just a fake shell for that. And so our invokeAudio is gonna be that callback actor that we talked about. So we have sendBack and receive.
And so this actor can receive a play event which causes it to play and it could receive a pause event which causes it to call its pause method. And the audio or at least our fake audio interface also has an addEventListener method with timeUpdate. And so this is useful because it gives us both the current time and the duration of the audio which is both useful information that we could incorporate directly into our state machine.
So that’s invokeAudio, we also have loadSong which loads a new song every second. So now you’d no longer have to do that loaded event manually. We’re gonna be invoking a promise instead. And this promise is pretty simple, you could easily replace this with a fetch function. It’s going to give us the data of the song, the title, artist, and the duration after one second.
And it’s just incrementing the song counter and having a random duration for each song. So we want to do two main things in this machine. In this loading state, we want to invoke that promise. And in onDone, we want to go back to that ready.hyst.state that we defined in the previous lesson.
Now in the ready state, this is where we want to invoke the audio callback. Because up until this moment, we’ve been defining the behavior of playing, paused, and also it going finished. But of course without side effects which is this invoking of the audio callback, our media player really does nothing.
So we want it to actually do something and that’s where invoking that callback, which is that invokeAudio function, comes into handy. [COUGH] So there is also the two things over here where we want to send events to that invoked audio actor which again is an invoked callback. So this playAudio actually needs to be an action that sends the play events to that playAudio.
And the pauseAudio action should be an action that sends the pause event to the audio. And remember you could send an event or create that action object using send and the events and to the invoked ID, or the invoked actor ID.
Actor Model Solution
All right, so let’s make our app come alive with actors. So we are going to go to lesson 9 actors and make this actually work. So we have to do a couple things. First we have to invoke a promise. And so this is going to be that invoke syntax, where we have invoke, we specify in a source.
And so that source is going to be something that returns this loadSong. And actually, we could just have this be loadSong just because it’s the same thing as doing this. But yeah, we’ll just keep it as this. And so when this loadSong is done, then we want to transition to the ready.hist target.
And we also wants to assign that song data. So that’s going to be if we remember we have this assignSongData right over here, so, we are going to assignSongData. Now let’s see if this works. Well looks like it does work. So when I reload after a second, it gives us a random song with a random duration.
But this isn’t quite working yet. So now let’s add that, we’re done with this. And now we want to invoke in this ready state, we are going to invoke the audio callback. So, I’m going to give this an idea of audio just so that we can send events to it.
And we’re also going to give it a source of this invokeAudioCallback, which already has this part here for taking in the context and the events. So, we could use it directly. InvokeAudio, and this is not going to take onDone or onError or anything like that, just because this is a continuous thing.
And something to mention here too, in this audio callback, we’re receiving an event and doing something based on whether we receive a play or pause event from the machine. But, it’s also good to do some cleanup. And I want to just make sure that the cleanup function is being called.
So I’m just going to pretend that this disposes of the event listener here and does any other things that we need to do to clean it up. All right, so now that should be working. So when I press PLAY and PAUSE, nothing’s happening quite yet. And the reason is because we have this, yeah, we have to assign the time.
And actually, it should be sending that assign time over there. So let’s just make sure that that’s working. Yep, we have sent back audio.time. And that’s because right now, nothing is playing. We haven’t sent that play event to the audio and so we need to just hook those up and then everything should work.
So we are going to send, make sure we have the send function, yes. Okay, so we’re going to send the play event and we’re gonna send it to the audio and we’re gonna just specify the ID of that. And so we’re gonna be doing the same with PAUSE.
So again, we are specifying that the playAudio action sends the play event to the audio, the pauseAudio action sends the pause event to the audio. So let’s try it out. We’re paused right now. If we play, you see this is actually counting down. And so this is working.
Now if we pause, it pauses because it’s telling the audio to pause. So it’s going to do the right thing and not send it any time update events. Now check this out, if I skip a song notice how the cleanup function is called. And the cleanup function is called because we are going out of the of the ready states.
And so this invokeAudio is going to be disposed and so when it’s disposed, the dispose function gets called. And it’s no longer active when it’s outside of the ready state. So we load the next song, and then we start invoking the audio again. So you see even while it’s playing, We’re going to skip the song.
I’m logging way too many things, so I skipped the song. It does the clean up and then it’s going to go to the next song and do our previous behavior of having those history states in there. So now this is a fully functional media player. We could even click here to dislike the song and then go to the next song.
And we could of course toggle the volume on and off, just like we’ve done with our parallel states.
Actor Model Q&A
One question asked is in a nutshell, what is the difference between a service in an actor? The X term service to mean a machine that’s interpreted in made into an actor. So, really, there is no difference between a service and an actor right now. A service and an actor are both things where you could send events to.
They could spawn other actors, they could change their own behavior based on what events comes in. And an interesting bit of trivia is that the actor model came into X state later. And so before the actor model, we just called things services. And that’s because that’s what SC XML called, the things that were or the the interpreted machines and the things that are invoked.
However, they are exactly the same thing. And so when we rewrite the documentation and we move on to version five, that terminology is going to be consolidated. But yeah, a service is an actor. And you could just think of services as actors with state machine behavior. Someone asked can you repeat the difference between spawn and invoke?
So when you invoke an actor, that actor is alive for the duration of what state it’s in. We service in the example over here, where this audio actor is only alive while we’re in the ready state which is playing or paused. When we skip, that actor gets disposed and then when we go back to the ready state, we have a brand new actor.
When you spawn an actor, that actor is alive until the machine itself is stopped. And so you could also manually stop in a spawned actor. But essentially they’re both just actors. It’s just one has a different life cycle than the other one. Now the use case of where you will want to spawn an actor which again I encourage you to read up on it.
In the state documentation versus invoking an actor is when you need a dynamic number of actors. For example, let’s say that we have a playlist machine. And the playlist spawns dynamically a number of actors and each of these actors are song actors. So we have song, song, song.
This would be a good candidate for spawning actors because invocations are all about, like there being one or a finite number. A predetermined number of actors that represents what’s happening in the current states. For these spawn actors, they’re not directly tied to a state and there could be just a dynamic number of them.
So we could read from context and be like, okay, I have our playlist. We need to spawn the song actor for each one of these and send events to them individually. That would be a use case for spawned actors which are not tied to a state. Is it possible to send in events from outside any machine if we know the actors ID?
so in general no you cannot do that you need a reference to the actor itself. Thankfully, you can can’t have a reference to that actor from from here. So I’m actually gonna type service. Actually, you know what, let’s pause this. And let’s actually grab it just so I could show you.
Let’s see we have window that service. Okay, yeah, so I’m gonna say service.subscribe. Or actually let’s just log and see. So service.subscribe, state and we’re going to console.log the state. So this date has a property called children. In this these children, each one of these is the invoked actor.
So we have an audio actor and it has a simple interface, just like we built with get snapshot. We have sent, we have subscribe, we have the idea of it, and so you can access this directly and just send it’s stuff. So actually, this is interesting. So right now, we are in the past states but what I could do is if I get direct access to this states.
I could say temp1.children.audio.send, and I could send it a play events. And so now even though I’m in the past date, it is playing. And so this just goes to show you. Obviously, this should be in the impossible state the song should not be playing. Sorry, impossible behavior, because the song should not be playing while we’re in the pause state.
And the reason this happened is because as a human hacker directly got access to that actor. And I sort of sent it information and I told it what to do outside of the parents. So that’s like if you again think of this in terms of humans it’s like your manager telling you what to do but then someone completely outside the company sneaking into the company and being like hey you should do this instead.
So, obviously that’s going to cause some problems. And that’s why you shouldn’t be just randomly sending actors events. You should be doing so in a controlled way. So I could pause it. Yeah, so you see what happens when we have direct access to an actor or try to access an actor directly when it’s not part of the system that we created.
Can I have two separately created machines interact with each other as actors? And so the problem with that is that if you have, let’s say, a machine here, in the machine here, they have no way of knowing about each other. You need some sort of glue layer on top of here that sort of knows about both machines and can Past events back and forth between them.
But that becomes an implicit way of modeling that. So instead it’s recommended that you have a parent’s machine or a parent’s actor. However, you want to implement it in this parents actor. Is responsible for spawning both machines and then what it could do is when it spawns the machine.
This parent machine can send a reference to this actor. So if I give this a blue background and this machine or this actor, sort of sense, a reference to that actor as a message then let’s lower the opacity. Then now this actor can talk to that actor directly because this parents actor told it about that sibling actor so that’s sort of one way of modelling it.
Another way is that you can have this sending events to the parents and make that full. And so that parents can routes that events to the other actor and so this parents essentially becomes a router.
Testing
The final lesson is more open ended. We’re gonna be talking about strategies for testing the state machines that we built. We’re also gonna be talking about the flip side of that in using state machines for what’s known as model based testing, in which we could test any application whether they’re using state machines or not.
And then the floor is open to other topics and questions that you may have. So the machines that we created are pretty amenable to testing, and there is a guide in the documentation for how exactly to test these machines. So remember that the machines have a transition function and this transition function is a pure function.
So probably the simplest way to test this is to have an expected value. So for example, let’s say we have a traffic light and the expected value is yellow. So if we transition, if we have a state of green and a timer event happens, then we expect that this state should be yellow.
So we could do actual state.matches yellow and check if it’s truthy. And so basically the same way that you would unit test functions, you can unit test state machines by unit testing the transition function. And you don’t need to just verify that the state is correct you could also verify that the actions that the state is going to execute is correct, you can verify that it receives the right event and other things too.
Now, in testing services is where it gets a little bit more intersting. So when you test services these should really be treated as async tests, so you could use a promise, or what I’d like to do is in Jest or Mocha you could use done. And so basically for example, in this machine, what we’re saying is that we want to test that the machine eventually reaches a certain state.
And so that’s what we’re doing in here, we have this on transition callback that is called whenever the state transitions. And so we could check that if the machine eventually ends up in this success state, then we’re done. And so then the test succeeds. What we could also do is we could have an expect call over here and make additional assertions on that state.
And then Jest has this feature where you could say expect.assertions and see the number of assertions that you expect to happen. So again, testing services is more of an acing thing and testing the actual machine logic can be done in your normal unit testing fashion. Now, because machines are configurable all of the implementation details can really be mocked.
So in the machine basically has a built in way of doing this. So just using the same width config callback that we’ve been using in previous exercises, you can provide alternatives for actions, services, guards, delays and other things, especially in the future. And so that way you can mock responses for example if we have a fetch from API service that’s invoked.
We could provide a mock response instead of actually trying to mock that call. So things become a lot easier when it’s all contained in the same machine. So those are the general strategies for testing the application logic. Another strategy is using the state machine to test applications that might not use state machines at all.
In this article I wrote we’re testing a feedback flow where we have three steps, how was your experience? You could answer good or bad. If you click bad, it asks you why? And then you could submit it. If you answer good, it says thanks for your feedback and then we could close it from this button, or this button, or that button.
So this app may or may not be implemented with XState or state machines, we might just be implementing it with the old ways that we know. But we could still use state machines to test the application. So in here there’s a bunch of tests and especially end to end tests that result from this.
We have as a user if I’m on the how was your experience screen, and I click good, I should be on the things for your feedback screen. Similarly, as a user if I’m on the form screen and I press submit, then I should be on the things for your feedback screen.
Or if I’m on the form screen and I press escape, or press this little x over here then I should be in the closed state. And so, instead of specifying all of these end to end tests one by one which is really hard to do exhaustively and takes an enormous amount of time and effort.
It’s better to represent it as a model instead or a test model. So this test model is a state machine, and the state machine represents all of your given when then statements in a single state machine. So now we could see each of our specifications are represented completely in this machine.
For example, if we’re on the question state and we click bad, now we’re on the form state and so on and so forth. So this could be represented using XState, and then using a library called X dates test. We could take that machine, provide test data, and asserts that we are in each state for that test data.
So it also provides a way to execute events. And so what this is going to do, and I’ll just scroll down here, it’s going to generate all the possible paths through the machine depending on the configuration parameters you set. Like how exhaustive you want these tests to be, and it’s going to try to traverse the entire state machine and test all of these paths.
And make sure that your app which again might or might not use XState matches the specification that you modeled in that test state machine. And so, while the learning curve here is pretty much the same as learning state machines and hopefully in this workshop you surmounted that learning curve.
It really shows how state machines can be useful in other areas rather than just application, implementation. So for example, this is a video showing puppeteer running and going through all of those test cases, and manually, or automatically entering that information for you, and making sure that every state is what we expect.
So I encourage you to research model based testing and see how you can use state machine and XState tests to test your existing applications.
Wrapping Up
Wrapping Up
I also wanted to mention that if you want just a collection of state machines that you just want to look at for inspiration or see how other people are modeling their application logic. There is a registry at stately.ai/registry with a ton of different machines, for example, here’s an offline queue.
And so this is going to open in the visualizer and just show the machine over here. So that you could look at it and just really understand what’s going on especially by playing around with the machine, sending events and seeing what happens. So I recommend you take a look at the registry, there’s also xstate-catalogue, the British spelling.com.
And so in this catalog there are a bunch of, I guess, handpicked state machines where they’re actually described for you. So anything that you might want to do, such as creating a multi-step form, is in the catalog. And so it talks about just the different states that it could be and the different events that can happen.
And basically, it gives you an overview of how you would model a state machine in order to accomplish whatever tasks you might want to do in an application. So I definitely recommend you check out the xstate catalogue and the xstate registry. Also, you can go to stately.ai/biz at any time for just playing around with state machines and you can save them as well.
You just log in via GitHub, and then your machines are accessible when you log in. So yeah, we’re gonna be coming out with a lot more tools in the future too. The one that I’m most excited about is a visual state machine and state chart editor. Where just like I’ve been doing in ExcaliDraw for this whole workshop instead I could do in a state machine editor that makes it easier to create these state machines and export them directly to code.
And also integrate them with your existing state machine code in your applications.
We’ve talked from the beginning about given when then syntax, and then you just showed model based I think it’s really cool that you can iterate through all those paths. Would there be a pro and con of that versus vanilla Cucumber?
If you wanted to write that and then have some other mechanism writing in plain English with the given when then like you started out and having it maybe use Puppeteer or Cypress to interact with machines for integrated testing?
So the question was about using these given when statements instead of just a model based testing approach where you find it all in one.
And honestly both of them are pretty much one in the same, you can do it either way. You could have separate given when then tests or you could represent it all as a test model. So the test model in model based testing is basically all of those given when then specifications combined into one machine.
So instead of saying on a, if b then c and on a if c then d and on b if e then g, it’s just represented as one model where you could derive all of the given when then statements from that state machine. So they’re really one and the same.
And especially if you don’t want to code that state machine up front, then you can just continue to use Cucumber given when then and create all of those test statements. And then later, take a look at each of those specifications and put them into a state machine so that you can merge all of them together.
And I think that that’s actually a really good technique and a good way of organically discovering how you can model your application with these specifications. In fact, I think Bob Martin talked about how these specifications are essentially a state machine anyway. So it’s just different ways of writing the same state machine.
What are your recommendations for model based testing with state machines that use actors? So one way you could do this is you can mock the actors. But what I also recommend is if you go to the Xstate repo, which is @statelyai/xstate, if you look at the test files, and we look@actor.test.ts.
You’re going to see an example of just the different ways that you can test these actors. So something I’d like to do is when testing the actors I just make sure that when we test them that the actors either respond immediately or respond with something that doesn’t require any outside interaction with the actor.
And basically, I’m testing in my parent state machine that it eventually reaches a final state or that it reaches some specific state. And what’s great about this is that I can call onDone in interpret, and so this is a callback that’s called when this machine reaches its top level final state.
And so, I know if I reach that top level final state, then the interaction between the state machine and the actor was successful. So yeah, it’s just a really easy way of testing that. But in general, you can mock actors, and that’s something that I’d recommend unless you’re doing integration testing, then yeah.
Again, if you have a direct reference to that actor, which you do in state.children, or if you’re spawning in state.context directly. Then you can send events to that actor to sort of coerce it to behave in a certain way and send events back to the parents potentially. All right, there was a good question in the chat, can context be serializable or can you store anything in context?
So unofficially, you can store anything in context. And especially in just practical use cases, I love using maps and sets and custom data structures, things that are not really directly serializable. So serializability is really important for when you’re actually using developer tooling. So that you can take that initial context and you can present it in a way where you can ship it as JSON or maybe even converted to SC XML somewhere.
And these target locations really don’t have a way of understanding a map or a set because there’s no way to serialize them cleanly. However with xstate inspect, we are coming up with a new way to serialize custom context. So even if you have custom data structures like maps and sets you should be able to tell the serializer, I want to serialize these in this specific way.
So that in the developer tooling, it’s shown either as null or as just the string map, or just as a custom data structure that is serializable and that you can understand in the developer tooling. So in short, put whatever you want into context, but just keep in mind when you’re using developer tools, that machine definition is sent over the wire to whatever inspector you’re using.
And it needs to show that somehow, and obviously JSON and JSON serializable things are the easiest way for the inspector to do that. Today, we covered a lot. We talked about software modelling event driven architecture, and the why behind using state machines and state charts to describe your application logic.
We went over the fundamental parts of state machines and state charts and how to use them effectively in your application logic, especially to improve it in a natural way and an easily visualizable way. We also talked about the actor model and how it’s important to conceptually view your app as different actors talking to each other, just for a proper separation of concerns.
And we talked about other topics as well, such as using state machines for things other than implementing your application logic like testing your app. And just some developer tooling that you can use to make state machines that much more useful in your application. I hope that you surmounted the learning curve of XState and learned about state machines and state charts and how useful they could be in your application.
And I wanna thank you all for joining this workshop.
xstate
第一个状态机
可直接参考官方api
入门 | XState 文档 (lecepin.github.io)
新建vue
项目,新建src/utils/xstate.js
,按照上述教程入门
1 | import { createMachine } from "xstate"; // 这是一个创建状态机的函数。 |
vscode
中安装可视化插件,装完后右下角有个提示,点击允许
上述代码给定了id
、初始状态、子状态、transitions
、最终状态,此时就可以点击下图所示按钮,进行可视化查看
运行状态机
如何运行我们的状态机,取决于我们计划在哪里使用它
浏览器中使用
你可以直接从 unpkg CDN (opens new window)中包含 XState:
XState core: https://unpkg.com/xstate@4/dist/xstate.js
XState web:https://unpkg.com/xstate@4/dist/xstate.web.js
- 浏览器兼容,ES module 构建
1
<script src="https://unpkg.com/xstate@4/dist/xstate.js"></script>
变量
XState
将在全局范围内可用,这将使你能够访问顶级导出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/xstate@4/dist/xstate.js"></script>
<script>
const {
createMachine,
actions,
interpret
} = XState;
const promiseMachine = createMachine({
id: "promise",
initial: "pendding",
states: {
pendding: {
on: {
RESOLVE: {
target: "resolved"
},
REJECT: {
target: "rejected"
},
},
},
resolved: {
type: "final"
},
rejected: {
type: "final"
},
},
});
const promiseServlice = interpret(promiseMachine).onTransition(state => {
console.log(state.value);
});
promiseServlice.start();
promiseServlice.send({
type: 'RESOLVE'
});
</script>
</body>
</html>
在 Node/Vanilla JS
为了 解释(interpret)
状态机并使其运行,我们需要添加一个解释器。这将创建一个服务:
1 | import { createMachine, interpret } from 'xstate'; |
与Vue
一起使用
与 Vue 一起使用 | XState 文档 (lecepin.github.io)
- Install
xstate
and@xstate/vue
:
1 | npm i xstate @xstate/vue |
Vue follows a similar pattern to React:
- The machine can be defined externally;
- The service is placed on the
data
object; - State changes are observed via
service.onTransition(state => ...)
, where you set some data property to the nextstate
; - The machine’s context can be referenced as an external data store by the app. Context changes are also observed via
service.onTransition(state => ...)
, where you set another data property to the updated context; - The service is started (
service.start()
) when the component iscreated()
; - Events are sent to the service via
service.send(event)
.
定义js
1 | import { createMachine } from 'xstate'; |
定义组件
1 | <!-- Toggle.vue --> |
状态机和状态图简介
状态图(statecharts)是一种图形语言,它用来描述过程中的状态。
你可能也用过类似的图,来设计用户流程图、规划数据库、或者构建 APP 架构。状态图(statecharts)是换种方式,用一堆盒子和箭头,来给人展示什么叫流程。不过,有了 XState,我们就能用代码来管理应用逻辑了。
这篇指南,会用初学者友好的方式,给你讲讲状态图(statecharts) 基础,内容如下:
- 状态 states
- 转换与事件 transitions and events
- 初始状态 initial states
- 最终状态 final states
- 复合状态 compound states
- 并行状态 parallel states
- 自转换 self-transitions
- 计划状态图 planning statecharts
- 延迟状态图 delayed transitions
- 动作 actions
状态 States
我们用圆角矩形盒子来展示 状态。为狗的过程,绘制状态图,首先会想到两种状态
狗总是 睡着(asleep) 或 醒着(awake)。狗不能同时睡着和醒着,狗也不可能不睡不醒。只有这两种状态,没其它的了,这就是我们说的有限数量的状态。
转换与事件 Transitions and event
狗在 睡着 和 醒着 之间的变化,是通过转换来表示的,它用一个箭头表示,从一个状态指向过程序列中的下一个状态
转换(transition)是由导致状态更改的 事件(event) 引起的。用事件来标记转换。
转换和事件是 确定性 的。 确定性意味着每个转换和事件总是指向相同的下一个状态,并且每次进程运行时总是从给定的起始条件产生相同的结果。 你永远不会把狗摇醒后,它还 睡着 ,或打晕它 它还 醒着 吧。
小狗具有两个有限状态,和两个转换的过程,就是一个 有限状态机。 状态机用于描述某事物的行为。 状态机描述事物的状态,以及这些状态之间的转换。 它是一个有限状态机,因为它具有有限数量的状态。(缩写为 FSM)
初始状态 Initial state
任何具有状态的事物,都会有一个 初始状态,即进程存在的默认状态,直到发生事件,从而改变事物的状态。
初始状态用实心圆圈表示,箭头从圆圈指向初始状态
用状态图来描述遛狗的过程,初始状态会是 等待(waiting) 走路。
最终状态 Final state
大多数具有状态的进程都会有一个 最终状态,即进程完成时的最后一个状态。 最终状态由状态圆角矩形框上的双边框表示。
在遛狗状态图中,最终状态是 溜狗完成(walk complete)。
复合状态 Compound states
复合状态是可以包含更多状态的状态,也称为子状态。 这些子状态只能在父级复合状态发生时发生。在遛狗(on a walk)状态中,可以有 走路中(walking)、 奔跑中(running) 和 停下来闻闻好闻的气味(stopping to sniff good smells) 几个子状态。
复合状态由标记的圆角矩形框表示,该框充当其子状态的容器。
复合状态还应指定哪个子状态是初始状态。 在 on a walk 状态下,初始状态为 walking。
复合状态使状态图能够处理比日常状态机更复杂的情况。
原子状态 Atomic states
原子状态是没有任何子状态的状态。等待(Waiting), 遛狗完成(walk complete), 走路(walking), 奔跑(running) 和 停下来闻闻好闻的(stopping to sniff good smells) 都是原子状态。
并行状态 Parallel states
并行状态是一种复合状态,其中所有子状态(也称为区域)同时处于活动状态。 这些区域在复合状态容器内由虚线分隔。
在 on a walk 复合状态内,可能有两个区域。 一个区域包含狗的 walking、 running 和 stopping to sniff good smells 的活动子状态,另一个区域包含狗的尾巴 摇动(wagging) 和 不摇动(not wagging) 状态。 狗可以走路和摇尾巴,跑和摇尾巴,或者在摇尾巴的同时停下来闻,它也可以在不摇尾巴的情况下进行任何这些活动。
两个区域还应该指定哪个子状态是初始状态。 在我们的 tail 区域,初始状态是 not wagging。
自转换 Self-transition
自转换是指事件发生但转换返回到相同状态时。 转换箭头退出并重新进入相同的状态。
描述自我转变的一种有用方法是在过程中“一直做某事,但一直没变化”。
在狗讨好的过程中,会有一个 讨好(begging) 状态和一个 获得好处(gets treat) 事件。 而对于爱吃的狗来说,无论你经历了多少次得到 gets treat 事件,狗都会回到 begging 状态。
计划状态图 Planning statecharts
状态图的好处之一是,在将状态图放在一起的过程中,你可以发觉过程中的所有可能状态。 这种探索将帮助你避免代码中的错误,因为能让你覆盖到所有的事件变化。
而且由于状态图是可执行的,它们既可以作为图表,也可以作为代码,从而减少在图表和编码环境之间引入差异或错误解释的可能性。
为登录状态机计划一个状态图 Planning a statechart for a login machine
要绘制登录状态机的状态图,首先要列出流程中的基本事件。 想想你的登录过程会 做 什么:
- 登进 log in
- 登出 log out
然后列出由于这些事件而存在的 状态:
- 已登进 logged in
- 已登出 logged out
一旦有了一些事件和状态,状态图就开始了。
不要忘记 初始状态。 在这种情况下,logged out 状态是初始状态,因为任何新用户都会进入未登录过程。
延迟转换 Delayed transitions
作为安全措施,某些登进和登出的过程,会在固定时间后,登出非活动用户。
活动(active) 和 空闲(idle) 状态仅在用户登进时发生,因此它们成为 登进(logged in) 复合状态中的子状态。
logged in 复合状态中的初始状态是 active,因为它是 log in 事件的直接结果,登录是用户活动的标志。
延迟转换(delayed transition) 是一种在处于某种状态,达到指定时间长度后,发生的转换。 延迟的转换被标记为“之后”和一个固定的持续时间,以指示在转换到下一个指示状态之前应该经过多长时间。
在登进状态图中,60000 毫秒或 1 分钟的延迟转换跟随 active 状态来确定用户是否 idle。 如果在转换达到一分钟之前有 activity 事件,则流程返回 active 状态。
如果用户保持 idle 状态,则在空闲状态之后会延迟 180000 毫秒(或 3 分钟)转换到 自动登出(auto logged out) 状态。
动作 Actions
状态图使用,在状态图之外系统触发的 actions。 动作通常也称为 作用(effects) 或 副作用(side-effects)。 “副作用”听起来像是一个消极或不重要的术语,但引发动作,是使用状态图的主要目的。
动作事件,对后续的其余部分没有影响,事件只是被触发,流程还是原来设置的那样,走下一步。 例如,登录状态图可能会执行更改用户界面的操作。
可以在进入或退出状态或转换时触发 动作。状态的操作包含在状态容器内,带有“entry /” 或 “exit /”标签,具体取决于动作是在进入还是退出状态时触发。
在登录状态图中,idle 状态有一个进入动作来警告用户他们可能会被登出。
状态机 Machines
状态机是一组有限的状态,可以根据事件确定性地相互转换。 要了解更多信息,请阅读 介绍状态图。
配置
状态机和状态图都是使用 createMachine()
工厂函数定义的:
1 | import { createMachine } from 'xstate'; |
状态机配置与 状态节点配置 相同,增加了上下文(context)属性:
代表状态机所有嵌套状态的本地“扩展状态”。 有关更多详细信息,请参阅文档 context 文档。
选项
actions、 activities、 delays、 guards、 和 services 的实现可以在状态机配置中作为字符串引用,然后在 createMachine()
的第二个参数中指定为对象:
1 | const lightMachine = createMachine( |
该对象有 5 个可选属性:
actions
- action 名称到它们的执行的映射activities
- activities 名称与其执行的映射delays
- delays 名称与其执行的映射guards
- 转换守卫 (cond
) ,名称与其执行的映射services
- 调用的服务 (src
) ,名称与其执行的映射
扩展状态机
可以使用 .withConfig()
扩展现有状态机,它采用与上述相同的对象结构:
1 | const lightMachine = // (同上面的例子一样) |
初始化 Context
如第一个示例所示,context
直接在配置本身中定义。 如果要使用不同的初始 context
扩展现有状态机,可以使用 .withContext()
并传入自定义 context
:
1 | const lightMachine = // (像第一个例子) |
注意
这 不会 对原始
context
进行浅层合并,而是将原始context
替换为.withContext(...)
的context
。 你仍然可以通过引用machine.context
手动“合并”上下文:
1
2
3
4
5 const testLightMachine = lightMachine.withContext({
// 合并原始 context
...lightMachine.context,
elapsed: 1000
});
状态 State
状态是系统(例如应用)在特定时间点的抽象表示。 要了解更多信息,请阅读 状态图简介中的状态部分。
API
状态机的当前状态由一个 State
实例表示:
1 | const lightMachine = createMachine({ |
State 定义
State
对象实例是 JSON 可序列化的,并具有以下属性:
value
- 当前状态的值。(例如,{red: 'walk'}
)context
- 当前状态的 contextevent
- 触发转换到此状态的事件对象actions
- 要执行的 动作 数组activities
- 如果 活动 开始,则活动映射到true
,如果活动停止,则映射到false
。history
- 上一个State
实例meta
- 在 状态节点 的元属性上定义的任何静态元数据done
- 状态是否表示最终状态
State
对象还包含其他属性,例如 historyValue
、events
、tree
和其他通常不相关并在内部使用的属性
State 方法和属性
state.matches(parentStateValue)
state.matches(parentStateValue)
方法确定当前 state.value
是否是给定 parentStateValue
的子集。 该方法确定父状态值是否“匹配”状态值。 例如,假设当前 state.value
是 { red: 'stop' }
:
1 | console.log(state.value); |
提示
如果要匹配多个状态中的一个,可以在状态值数组上使用
.some()
来完成此操作:
1
2
3 const isMatch = [{ customer: 'deposit' }, { customer: 'withdrawal' }].some(
state.matches
);
state.nextEvents
state.nextEvents
指定将导致从当前状态转换的下一个事件:
1 | const { initialState } = lightMachine; |
state.nextEvents
在确定可以采取哪些下一个事件,以及在 UI 中表示这些潜在事件(例如启用/禁用某些按钮)方面很有用。
state.changed
state.changed
指定此 state
是否已从先前状态更改。 在以下情况下,状态被视为“已更改”:
- 它的值不等于它之前的值,或者:
- 它有任何新动作(副作用)要执行。
初始状态(没有历史记录)将返回 undefined
。
1 | const { initialState } = lightMachine; |
state.done
state.done
指定 state
是否为“最终状态” - 最终状态是指示其状态机已达到其最终状态,并且不能再转换到任何其他状态的状态。
1 | const answeringMachine = createMachine({ |
state.toStrings()
state.toStrings()
方法返回表示所有状态值路径的字符串数组。 例如,假设当前 state.value
是 { red: 'stop' }
:
1 | console.log(state.value); |
state.toStrings()
方法对于表示基于字符串的环境中的当前状态非常有用,例如在 CSS 类或数据属性中。
state.children
state.children
是将生成的 服务/演员 ID 映射到其实例的对象。 详情 📖 参考服务。
使用 state.children
示例
1 | const machine = createMachine({ |
state.hasTag(tag)
从 4.19.0 开始
state.hasTag(tag)
方法,当前状态配置是否具有给定标签的状态节点。
1 | const machine = createMachine({ |
例如,如果上面的状态机处于 green
或 yellow
状态,而不是直接使用 state.matches('green') || state.matches('yellow')
,可以使用 state.hasTag('go')
:
1 | const canGo = state.hasTag('go'); |
state.can(event)
从 4.25.0 开始
state.can(event)
方法确定一个 event
在发送到解释的(interpret
)状态机时,是否会导致状态改变。 如果状态因发送 event
而改变,该方法将返回 true
; 否则该方法将返回 false
:
1 | const machine = createMachine({ |
如果 state.changed
为 true
,并且以下任何一项为 true
,则状态被视为“changed
”:
state.value
改变- 有新的
state.actions
需要执行 state.context
改变
持久化State
如前所述,可以通过将 State
对象序列化为字符串 JSON 格式来持久化它:
1 | const jsonState = JSON.stringify(currentState); |
可以使用静态 State.create(...)
方法恢复状态:
1 | import { State, interpret } from 'xstate'; |
然后,你可以通过将 State
传递到已解释的服务的 .start(...)
方法,来从此状态解释状态机:
1 | // ... |
这还将维护和恢复以前的 历史状态,并确保 .events
和 .nextEvents
代表正确的值。
注意
XState 尚不支持持久化生成的 演员(actors)
State
元数据
元数据,是描述任何 状态节点 相关属性的静态数据,可以在状态节点的 .meta
属性上指定:
1 | const fetchMachine = createMachine({ |
状态机的当前状态,收集所有状态节点的 .meta
数据,由状态值表示,并将它们放在一个对象上,其中:
- key 是 状态节点 ID
- value 是状态节点
.meta
的值
例如,如果上述状态机处于 failure.timeout
状态(由 ID 为 “failure”
和 “failure.timeout”
的两个状态节点表示),则 .meta
属性将组合所有 .meta
值,如下所示:
1 | const failureTimeoutState = fetchMachine.transition('loading', { |
提示:聚合元数据
你如何处理元数据取决于你。 理想情况下,元数据应 仅 包含 JSON 可序列化值。 考虑以不同方式合并/聚合元数据。 例如,以下函数丢弃状态节点 ID key(如果它们不相关)并合并元数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 function mergeMeta(meta) {
return Object.keys(meta).reduce((acc, key) => {
const value = meta[key];
// 假设每个元值都是一个对象
Object.assign(acc, value);
return acc;
}, {});
}
const failureTimeoutState = fetchMachine.transition('loading', {
type: 'TIMEOUT'
});
console.log(mergeMeta(failureTimeoutState.meta));
// => {
// alert: 'Uh oh.',
// message: 'The request timed out.'
// }
笔记
- 你永远不必手动创建
State
实例。 将State
视为仅来自machine.transition(...)
或service.onTransition(...)
的只读对象。 state.history
不会保留其历史记录以防止内存泄漏。state.history.history === undefined
。 否则,你最终会创建一个巨大的链表并重新发明区块链,而我们并不这样做。- 此行为可能会在未来版本中进行配置。
状态节点
状态机包含状态节点(如下所述),它们共同描述状态机可以处于的 整体状态。在下一节描述的 fetchMachine
中,有 状态节点,例如:
1 | // ... |
以及整体 状态,即 machine.transition()
函数的返回值或 service.onTransition()
的回调值:
1 | const nextState = fetchMachine.transition('pending', { type: 'FULFILL' }); |
什么是状态节点?
在 XState 中,状态节点 指定状态配置。 它们是在状态机的 states
属性上定义的。 同样,子状态节点在状态节点的 states
属性上分层定义。
从 machine.transition(state, event)
确定的状态,表示状态节点的组合。 例如,在下面的状态机中,有一个 success
状态节点和一个 items
子状态节点。 状态值 { success: 'items' }
表示这些状态节点的组合。
1 | const fetchMachine = createMachine({ |
状态节点类型
有五种不同类型的状态节点:
- atomic 原子状态节点没有子状态。 (即,它是一个叶节点。)
- compound 复合状态节点包含一个或多个子
states
,并有一个initial
状态,这是这些子状态之一的 key。 - parallel 并行状态节点包含两个或多个子
states
,并且没有初始状态,因为它表示同时处于其所有子状态。 - final 最终状态节点是代表抽象“终端”状态的叶节点。
- history 历史状态节点是一个抽象节点,表示解析到其父节点最近的浅或深历史状态。
可以在状态节点上显式定义状态节点类型:
1 | const machine = createMachine({ |
将 type
明确指定为 'atomic'
、'compound'
、'parallel'
、'history'
、或 'final'
有助于 TypeScript 中的分析和类型检查。 但是,它只需要 parallel、history 和 final 状态。
瞬间状态节点
一个瞬间状态节点是一个“直通”状态节点,它会立即转换到另一个状态节点; 也就是说,状态机不会停留在瞬间状态。 瞬间状态节点可用于,根据条件确定状态机应该从先前状态真正进入哪个状态。 它们与 UML 中的 选择伪状态最相似。
定义瞬间状态节点的最佳方法是,使用无事件状态和 always
转换。 这是一个转换,其中第一个为 true 的条件会立即被采用。
例如,这个状态机的初始瞬间状态解析为 'morning'
、'afternoon'
右 'evening'
,具体取决于时间(隐藏实现细节):
1 | const timeOfDayMachine = createMachine({ |
状态节点元数据
元数据,是描述任何 状态节点 相关属性的静态数据,可以在状态节点的 .meta
属性上指定:
1 | const fetchMachine = createMachine({ |
状态机的当前状态,收集所有状态节点的 .meta
数据,由状态值表示,并将它们放在一个对象上,其中:
- key 是 状态节点 ID
- value 是状态节点
.meta
的值
有关用法和更多信息,请参阅状 状态元数据。
标签 Tags
状态节点可以有 tags,这些标签是帮助描述状态节点的字符串术语。 标签是可用于对不同状态节点进行分类的元数据。 例如,你可以使用 "loading"
标签来表示哪些状态节点代表正在加载数据的状态,并使用 state.hasTag(tag)
确定一个状态是否包含那些标记的状态节点:
1 | const machine = createMachine({ |
事件 Event
事件是导致状态机从当前 状态 转换 到下一个状态的原因。 要了解更多信息,请阅读 状态图简介中的事件部分。
API
事件是具有 type
属性的对象,表示它是什么类型的事件:
1 | const timerEvent = { |
在 XState 中,只有 type
的事件可以由其字符串类型表示,作为速记:
1 | // 等于 { type: 'TIMER' } |
事件对象还可以有其他属性,代表与事件相关的数据:
1 | const keyDownEvent = { |
发送事件 Send Event
正如 转换向导 中所解释的,给定当前状态和事件,转换到定义的下一个状态,在其 on: { ... }
属性上定义。 这可以通过将事件传递给 transition 方法 来观察:
1 | import { createMachine } from 'xstate'; |
许多原生事件,例如 DOM 事件,是兼容的,可以直接与 XState 一起使用,通过在 type
属性上指定事件类型:
1 | import { createMachine, interpret } from 'xstate'; |
NULL 事件
注意
null 事件语法
({ on: { '': ... } })
将在第 5 版中弃用。应改用新的 always 语法
NULL 事件是没有类型的事件,一旦进入状态就会立即发生。 在转换中,它由一个空字符串 (''
) 表示:
1 | // 人为的例子 |
null 事件有很多用例,尤其是在定义 瞬间转换 时,状态(可能是 瞬间状态 的)立即根据 条件 确定下一个状态应该是什么:
1 | const isAdult = ({ age }) => age >= 18; |
转换 Transitions
转换定义了状态机如何对 事件 做出响应。 要了解更多信息,请参阅 状态图介绍 中的部分。
API
状态转换在状态节点的 on
属性中定义,:
1 | import { createMachine } from 'xstate'; |
在上面的例子中,当状态机处于 pending
状态并且它接收到一个 RESOLVE
事件时,它会转换到 resolved
状态。
状态转换可以定义为:
- 一个字符串,例如
RESOLVE: 'resolved'
- 具有
target
属性的对象,例如RESOLVE: { target: 'resolved' }
, - 转换对象数组,用于条件转换(请参阅 守卫)
状态机 .transition 方法
如上所示, machine.transition(...)
方法是一个纯函数,它接受两个参数:
它返回一个新的 State
实例,这是采用当前状态和事件,启用的所有转换的结果。
1 | const lightMachine = createMachine({ |
选择启用转换
启用的转换 是将根据当前状态和事件有条件地进行的转换。 当且仅当:
在 分层状态机 中,转换的优先级取决于它们在树中的深度; 更深层次的转换更具体,因此具有更高的优先级。 这与 DOM 事件的工作方式类似:如果单击按钮,则直接在按钮上的单击事件处理程序比 window
上的单击事件处理程序更具体。
1 | const wizardMachine = createMachine({ |
事件描述符
事件描述符,是描述转换 将匹配的事件类型的字符串。 通常,这等效于发送到状态机的 event
对象上的 event.type
属性:
1 | // ... |
其他事件描述符包括:
- Null 事件描述 (
""
),不匹配任何事件(即 “null” 事件),并表示进入状态后立即进行的转换 - 通配符事件描述 (
"*"
) 4.7+,如果事件没有被状态中的任何其他转换显式匹配,则匹配任何事件
自转换
自转换是当一个状态转换到自身时,它 可以 退出然后重新进入自身。 自转换可以是 内部 或 外部 转换:
- 内部转换 不会退出也不会重新进入自身,但可能会进入不同的子状态。
- 外部转换 将退出并重新进入自身,也可能退出/进入子状态。
默认情况下,具有指定目标的所有转换都是外部的。
有关如何在自转换上执行进入/退出操作的更多详细信息,请参阅有关 自转换的操作。
内部转换
内部转换是不退出其状态节点的转换。 内部转换是通过指定 相对目标(例如,'.left'
)或通过在转换上显式设置 { internal: true }
来创建的。 例如,考虑一台状态机将一段文本设置为对齐 'left'
、 'right'
、 'center'
、或 'justify'
:
1 | import { createMachine } from 'xstate'; |
上面的状态机将以 'left'
状态启动,并根据单击的内容在内部转换到其他子状态。 此外,由于转换是内部的,因此不会再次执行在父状态节点上定义的 entry
, exit
或者任何其他的 actions
。
具有 { target: undefined }
(或无 target
)的转换也是内部转换:
1 | const buttonMachine = createMachine({ |
内部转换摘要:
EVENT: '.foo'
- 内部转换到子状态EVENT: { target: '.foo' }
- 内部转换到子状态(以'.'
开头)EVENT: undefined
- 禁止转换EVENT: { actions: [ ... ] }
- 内部自转换EVENT: { actions: [ ... ], internal: true }
- 内部自转换,同上EVENT: { target: undefined, actions: [ ... ] }
- 内部自转换,同上
外部转换
外部转换 将 退出并重新进入定义转换的状态节点。 在上面的例子中,父级 word
状态节点(根状态节点),将在其转换时执行 exit
和 entry
动作。
默认情况下,转换是外部的,但任何转换都可以通过在转换上显式设置 { internal: false }
来实现。
1 | // ... |
上面的每个转换都是外部的,并且将执行父状态的 exit
和 entry
操作。
外部转换摘要:
EVENT: { target: 'foo' }
- 所有对兄弟状态的转换都是外部转换EVENT: { target: '#someTarget' }
- 到其他节点的所有转换都是外部转换EVENT: { target: 'same.foo' }
- 外部转换到自己的子级节点(相当于{ target: '.foo', internal: false }
)EVENT: { target: '.foo', internal: false }
- 外部转换到子节点- 否则这将是一个内部转换
EVENT: { actions: [ ... ], internal: false }
- 外部自转换EVENT: { target: undefined, actions: [ ... ], internal: false }
- 外部自转换,同上
瞬间转换
注意
空字符串语法 (
{ on: { '': ... } }
) 将在第 5 版中弃用。应该首选 4.11+ 版中新的always
语法。请参阅下面关于 无事件转换 的部分,它与瞬间转换相同。
瞬间转换是由 null 事件 触发的转换。 换句话说,只要满足任何条件,就会 立即 进行转换(即,没有触发事件):
1 | const gameMachine = createMachine( |
就像转换一样,可以将瞬间转换指定为单个转换(例如,'': 'someTarget'
)或条件转换数组。 如果没有满足瞬间转换的条件转换,则状态机保持相同状态。
对于每次内部或外部转换,始终 “sent” 空事件。
无事件 (“Always”) 转换 4.11+
无事件转换,是当状态机处于定义的状态,并且其 cond
守卫为 true
时 始终进行 的转换。 他们被检查:
- 立即进入状态节点
- 每次状态机接收到一个可操作的事件(无论该事件是触发内部转换还是外部转换)
无事件转换在状态节点的 always
属性上定义:
1 | const gameMachine = createMachine( |
无事件 vs. 通配符转换
- 通配符转换 在进入状态节点时不被检查。 无事件转换是,在做任何其他事情之前(甚至在进入动作的守卫判断之前)的转换。
- 无事件转换的重新判断,由任何可操作的事件触发。 通配符转换的重新判断,仅由与显式事件描述符不匹配的事件触发。
注意
如果误用无事件转换,则有可能创建无限循环。 无事件转换应该使用
target
、cond
+target
、cond
+actions
或cond
+target
+actions
来定义。 目标(如果已声明)应与当前状态节点不同。 没有target
和cond
的无事件转换将导致无限循环。 如果cond
守卫不断返回true
,则带有cond
和actions
的转换可能会陷入无限循环。
提示
当检查无事件转换时,它们的守卫会被重复判断,直到它们都返回 false,或者验证了具有目标的转换。 在此过程中,每当某个守卫判断为
true
时,其关联的操作将被执行一次。 因此,在单个微任务期间,可能会多次执行一些没有目标的转换。这与普通转换形成对比,在普通转换中,最多只能进行一个转换。
禁止转换
在 XState 中,“禁止”转换是一种指定不应随指定事件发生状态转换的转换。 也就是说,在禁止转换上不应发生任何事情,并且该事件不应由父状态节点处理。
通过将 target
明确指定为 undefined
来进行禁止转换。 这与将其指定为没有操作的内部转换相同:
1 | on: { |
例如,我们模拟所有事件都可以记录 log 数据,只在 userInfoPage 下不可以:
1 | const formMachine = createMachine({ |
提示
请注意,在分层嵌套状态链中定义具有相同事件名称的多个转换时,将只采用最内部的转换。 在上面的例子中,这就是为什么一旦状态机到达
userInfoPage
状态,父LOG
事件中定义的logTelemetry
动作就不会执行。
多个目标
基于单个事件的转换可以有多个目标状态节点。 这是不常见的,只有在状态节点合法时才有效; 例如,在复合状态节点中,转换到两个兄弟状态节点是非法的,因为(非并行)状态机在任何给定时间只能处于一种状态。
多个目标在 target: [...]
中被指定为一个数组,其中数组中的每个目标都是一个状态节点的相对键或 ID,就像单个目标一样。
1 | const settingsMachine = createMachine({ |
通配描述符 4.7+
使用通配符事件描述符 ("*"
) 指定的转换由任何事件激活。 这意味着 任何事件 都将匹配具有 on: { "*": ... }
的转换,并且如果守卫通过,则将采用该转换。
除非在数组中指定转换,否则将始终选择显式事件描述符而不是通配符事件描述符。 在这种情况下,转换的顺序决定了选择哪个转换。
1 | // 对于 SOME_EVENT,将显式转换到“here” |
提示
通配符描述符的行为方式与 瞬间转换(具有空事件描述符)不同。 每当状态处于活动状态时都会立即进行瞬态转换,而通配符转换仍然需要将某些事件发送到其状态才能触发。
示例:
1 | const quietMachine = createMachine({ |
FAQ
如何在转换中执行 if/else 逻辑?
有时,你会想说:
- 如果 something 是真的,就进入这个状态
- 如果 something else 为真,则转到此状态
- 否则,进入这个状态
你可以使用 守卫转换 来实现这一点
我如何转换到 任何 状态?
你可以通过为该状态提供自定义 ID 并使用 target: '#customId'
来转换到 任何 状态。 你可以在此处阅读有关 自定义 ID 的完整文档。
这允许你从子状态转换到父级的兄弟状态,例如在本例中的 CANCEL
和 done
事件中
分层状态节点 Hierarchical State Node
在状态图中,状态可以嵌套 在其他状态中 。 这些嵌套状态称为 复合状态。 要了解更多信息,请阅读状态图简介中的复合状态部分。
API
以下示例是具有嵌套状态的交通灯状态机:
1 | const pedestrianStates = { |
'green'
和 'yellow'
状态是 简单的状态 ——它们没有子状态。 相比之下,'red'
状态是复合状态,因为它由 子状态(pedestrianStates
)组成。
初始状态
当进入复合状态时,它的初始状态也立即进入。 在以下交通灯状态机示例中:
'red'
状态已进入- 由于
'red'
的初始状态为'walk'
,因此最终进入{ red: 'walk' }
状态。
1 | console.log(lightMachine.transition('yellow', { type: 'TIMER' }).value); |
事件
当前状态不处理 event
时,该 event
将传播到其要处理的父状态。 在以下交通灯状态机示例中:
{ red: 'stop' }
状态 不 处理'TIMER'
事件'TIMER'
事件被发送到处理该事件的'red'
父状态。
1 | console.log(lightMachine.transition({ red: 'stop' }, { type: 'TIMER' }).value); |
如果状态或其任何祖先(父)状态均未处理事件,则不会发生转换。 在 strict
模式下(在 状态机配置 中指定),这将引发错误。
1 | console.log(lightMachine.transition('green', { type: 'UNKNOWN' }).value); |
并行状态节点 Parallel State Node
在状态图中,你可以将状态声明为 并行状态。 这意味着它的所有子状态将同时运行。 要了解更多信息,请参阅 中的部分。
API
通过设置 type: 'parallel'
在状态机和/或任何嵌套复合状态上指定并行状态节点。
例如,下面的状态机允许 upload
和 download
复合状态同时处于活动状态。 想象一下,这代表一个可以同时下载和上传文件的应用程序:
1 | const fileMachine = createMachine({ |
最终状态
在状态图中,你可以将状态声明为 最终状态。 最终状态表示其父状态为“完成”。 要了解更多信息,请阅读我们对 状态图的介绍中的最后状态部分
API
要指示状态节点是最终节点,请将其 type
属性设置为 'final'
:
1 | const lightMachine = createMachine({ |
在复合状态中,到达最终子状态节点(使用 { type: 'final' }
)将在内部引发该复合状态节点的 done(...)
事件(例如,"done.state. light.crosswalkEast"
)。 使用 onDone
相当于为此事件定义一个转换。
并行状态
当并行状态节点中的每个子状态节点都 完成 时,父并行状态节点也 完成。 当到达每个子复合节点中的每个最终状态节点时,将为并行状态节点引发 done(...)
事件。
这在建模并行任务时非常有用。 例如,下面有一个购物机,其中 user
和 items
表示 cart
状态的两个并行任务:
1 | const shoppingMachine = createMachine({ |
onDone
转换只会在 'cart'
的所有子状态(例如,'user'
和 'items'
)都处于它们的最终状态时发生。 在购物机的情况下,一旦到达'shopping.cart.user.success'
和'shopping.cart.items.success'
状态节点,状态机将从'cart'
过渡到 'confirm'
状态。
注意
不能在状态机的根节点上定义
onDone
转换。 这是因为onDone
是对'done.state.*'
事件的转换,当状态机达到最终状态时,它不能再接受任何事件。
笔记
- 最终状态节点仅指示其直接父节点已 完成。 它不会影响任何更高父节点的 完成 状态,除非在其所有子复合状态节点 完成 时。
- 到达最终子状态的并行状态在其所有同级完成之前不会停止接收事件。 最后的子状态仍然可以通过事件退出。
- 最终状态节点不能有任何子节点。 它们是原子状态节点。
- 你可以在最终状态节点上指定
entry
和exit
动作。
作用 Effects
在状态图中,“副作用”可以分为两类:
“即发即弃”副作用,执行同步副作用,不将事件发送回状态图,或将 事件同步发送 回状态图:
- 动作(Actions) - 单一的、分散的作用
- 活动(Activities) - 退出它们开始所处的状态时处理的连续作用
调用作用,它执行一个可以 异步 发送和接收事件的副作用:
- 调用 Promises - 随着时间的推移,可能会
resolve
或reject
一次的单个分散副作用,这些作用结果,通过事件发送到父状态机 - 调用 Callbacks - 随着时间的推移可能会发送多个事件的持续副作用,以及监听直接发送给它的事件,到/从 父状态机
- 调用 Observables - 随着时间的推移,可能会发送由来自观察流的消息触发的多个事件的持续副作用
- 调用 Machines - 由
Machine
实例表示的连续副作用,可以发送/接收事件,也可以在达到 最终状态 时通知父状态机
动作 Actions
动作,是即发即弃的 作用。 它们可以通过三种方式声明:
entry
动作,进入状态时执行exit
动作,退出状态时执行- 执行转换时,执行转换的动作
要了解更多信息,请阅读 状态图简介中的动作
API
可以像这样添加动作
1 | const triggerMachine = createMachine( |
什么时候应该使用 转换 VS entry/exit 动作?
这取决于! 它们的做的事不同:
entry/exit 操作,意味着“在进入/退出此状态的任何转换上 执行此 动作”。 当 动作 只依赖于它所在的状态节点,而不依赖于上一个/下一个状态节点 或 事件时,使用进入/退出 动作
1
2
3
4
5
6
7
8
9
10
11
12
13// ...
{
idle: {
on: {
LOAD: 'loading'
}
},
loading: {
// 每当进入“loading”状态时执行此 动作
entry: 'fetchData'
}
}
// ...转换 动作 意味着“仅在此转换上 执行此 动作”。 当 动作 依赖于事件和它当前所处的状态节点时,使用转换 动作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// ...
{
idle: {
on: {
LOAD: {
target: 'loading',
// 此 动作 仅在此转换时执行
actions: 'fetchData'
}
},
loading: {
// ...
}
}
// ...
提示
可以通过直接在状态机配置中指定 动作 函数来快速原型化 动作 实现:
1
2
3
4
5
6 // ...
TRIGGER: {
target: 'active',
actions: (context, event) => { console.log('activating...'); }
}
// ...在状态机选项的
actions
属性中重构内联 动作 实现,可以更容易地调试、序列化、测试和准确地可视化 动作。
声明动作
从 machine.transition(...)
返回的 State
实例有一个 .actions
属性,它是一个供 解释(interpret) 执行的 动作 对象数组:
1 | const activeState = triggerMachine.transition('inactive', { type: 'TRIGGER' }); |
每个 动作 对象都有两个属性(以及其他可以指定的属性):
type
- 动作 类型exec
- 动作 执行函数
exec
函数有 3 个参数:
参数 | 类型 | 描述 |
---|---|---|
context |
TContext | 当前状态机的上下文 |
event |
event object | 导致转换的事件 |
actionMeta |
meta object | 包含有关 动作 的元数据的对象(见下文) |
actionMeta
对象包括以下属性:
参数 | 类型 | 描述 |
---|---|---|
action |
action object | 原始 动作 对象 |
state |
State | 转换后的已解析的状态机状态 |
解释(interpret)将调用带有 currentState.context
、event
和状态机转换到的 state
的 exec
函数。 你可以自定义此 动作。 阅读 执行 动作 了解更多详情。
动作顺序
在执行状态图时,动作的顺序不一定重要(也就是说,它们不应该相互依赖)。 但是,state.actions
数组中的操作顺序是:
exit
动作 - 退出状态节点的所有退出 动作,从原子状态节点开始- 转换
actions
- 在所选转换上定义的所有 动作 entry
动作 - 进入状态节点的所有进入 动作,从父状态开始
注意
在 XState 4.x 版中,
assign
动作 具有优先权,并且在任何其他 动作 之前执行。 此行为将在第 5 版中修复,因为将按顺序调用assign
操作。
警告
此处记录的所有 动作 创建者都返回 动作 对象; 它是一个纯函数,它只返回一个 动作 对象,并 不是 命令式的发送一个事件。 不要命令式的调用 动作 创建者; 因为 他们什么都不会做!
1
2
3
4
5
6
7
8
9
10
11 // 🚫 不要这样做!
entry: () => {
// 🚫 这将什么也不做; send() 不是命令式函数!
send({ type: 'SOME_EVENT' });
};
console.log(send({ type: 'SOME_EVENT' }));
// => { type: 'xstate.send', event: { type: 'SOME_EVENT' } }
// ✅ 这样替换
entry: send({ type: 'SOME_EVENT' });
发送动作(send action)
send(event)
动作 创建者创建了一个特殊的“发送” 动作 对象,它告诉服务(即,解释(interpret) 状态机)将该事件发送给它自己。 它在外部事件队列中,将一个事件排入正在运行的服务中,这意味着该事件将在 解释(interpret) 的下一步“步骤”上发送。
参数 | 类型 | 描述 |
---|---|---|
event |
string or event object or event expression | 发送到指定options.to (或self)的事件 |
options? |
send options (见下文) | 发送事件的选项。 |
send options
参数是一个包含以下内容的对象:
参数 | 类型 | 描述 |
---|---|---|
id? |
string | send ID (用于取消) |
to? |
string | 事件的目标(默认为self) |
delay? |
number | 发送事件前的超时时间(毫秒),如果在超时前没有取消事件 |
注意
send(...)
函数是一个 动作 创建者; 它是一个纯函数,它只返回一个 动作 对象,并 不会 命令式地发送一个事件。
1 | import { createMachine, send } from 'xstate'; |
传递给 send(event)
的 event
参数可以是:
- 一个字符串事件,例如
send('TOGGLE')
- 一个对象事件,例如
send({ type: 'TOGGLE', payload: ... })
- 一个事件表达式,它是一个函数,它接收触发
send()
动作 的当前context
和event
,并返回一个事件对象:
1 | import { send } from 'xstate'; |
发送目标
从 send(...)
动作 创建者发送的事件,可以表示它应该发送到特定目标,例如 调用 服务 或 创建 演员。 这是通过在 send(...)
操作中指定 { to: ... }
属性来完成的:
1 | // ... |
to
属性中的 target 也可以是一个 target 表达式,它是一个函数,它接受当前触发动作的 context
和 event
,并返回一个字符串 target 或一个 演员:
1 | entry: assign({ |
注意
同样,
send(...)
函数是一个 动作 创建者,不会命令式发送事件。 相反,它返回一个 动作 对象,描述事件将发送到的位置:
1
2
3
4
5
6
7
8
9 console.log(send({ type: 'SOME_EVENT' }, { to: 'child' }));
// logs:
// {
// type: 'xstate.send',
// to: 'child',
// event: {
// type: 'SOME_EVENT'
// }
// }
要从子状态机发送到父状态机,请使用 sendParent(event)
(采用与 send(...)
相同的参数)。
升高动作(raise action)
raise()
动作 创建者在内部事件队列中,将一个事件排入状态图。 这意味着事件会在 解释(interpret) 的当前“步骤”上立即发送。
参数 | 类型 | 描述 |
---|---|---|
event |
string or event object | 要提升的事件 |
1 | import { createMachine, actions } from 'xstate'; |
单击 visualizer 中的“STEP”和“RAISE”事件以查看差异。
响应动作 (respond action) 4.7+
respond()
动作 创建者创建一个 send()
动作,该 动作 被发送到,触发响应的事件的服务。
这在内部使用 SCXML 事件 ,从事件中获取 origin
,并将 send()
动作 的 to
设置为 origin
。
参数 | 类型 | 描述 |
---|---|---|
event |
string, event object, or send expression | 发送回发件人的事件 |
options? |
send options object | 传递到 send() 事件的选项 |
使用响应 action 的示例
这演示了一些父服务(authClientMachine
)向调用的 authServerMachine
发送一个 'CODE'
事件,并且 authServerMachine
响应一个 'TOKEN'
事件。
1 | const authServerMachine = createMachine({ |
详情请参阅 📖 发送响应。
转发动作(forwardTo action) 4.7+
forwardTo()
动作 创建者,创建一个 send()
动作,通过其 ID 将最近的事件转发到指定的服务。
参数 | 类型 | 描述 |
---|---|---|
target |
string or function that returns service | 要将最近事件发送到的目标服务。 |
使用 forwardTo 动作 的示例
1 | import { createMachine, forwardTo, interpret } from 'xstate'; |
错误升级动作(escalate action) 4.7+
escalate()
动作 创建者,通过将错误发送到父状态机来升级错误。 这是作为状态机识别的特殊错误事件发送的。
参数 | 类型 | 描述 |
---|---|---|
errorData |
any | 要升高(send)到父级的错误数据。 |
使用 escalate 动作 的示例
1 | import { createMachine, actions } from 'xstate'; |
日志动作(log action)
log()
动作 创建器是一种记录与当前状态 context
和/或 event
相关的任何内容的声明方式。 它需要两个可选参数:
参数 | 类型 | 描述 |
---|---|---|
expr? |
string or function | 一个简单的字符串或一个函数,它以 context 和 event 作为参数并返回一个要记录的值 |
label? |
string | 用于标记已记录消息的字符串 |
1 | import { createMachine, actions } from 'xstate'; |
没有任何参数,log()
是一个 动作,它记录一个具有 context
和 event
属性的对象,分别包含当前上下文和触发事件。
选择动作(choose action)
choose()
动作 创建者创建一个 动作,该 动作 指定应根据某些条件执行哪些 动作。
参数 | 类型 | 描述 |
---|---|---|
conds |
array | 当给定的 cond 为真时,包含要执行的 actions 的对象数组(见下文) |
返回:
一个特殊的 "xstate.choose"
动作 对象,它在内部进行判断以有条件地确定应该执行哪些动作对象。
cond
中的每个“条件动作”对象都具有以下属性:
actions
- 要执行的 动作 对象cond?
- 执行这些actions
的条件
注意
不要使用
choose()
动作 创建器来执行 动作,否则这些 动作 可能表示为通过entry
、exit
或actions
在某些 状态/转换 中执行的非条件 动作。
1 | import { actions } from 'xstate'; |
这类似于 SCXML <if>
、<elseif>
和 <else>
元素: www.w3.org/TR/scxml/#if
纯动作(pure action)
pure()
动作 创建器是一个纯函数(因此得名),它根据触发 动作 的当前状态“上下文”和“事件”返回要执行的 动作 对象。 这允许你动态定义应执行哪些 动作
参数 | 类型 | 描述 |
---|---|---|
getActions |
function | 根据给定的 context 和 event 返回要执行的动作对象的函数(见下文) |
返回:
一个特殊的 "xstate.pure"
动作 对象,它将在内部判断 get
属性以确定应该执行的 动作 对象。
getActions(context, event)
参数:
参数 | 类型 | 描述 |
---|---|---|
context |
object | 当前状态的 context |
event |
event object | 触发 动作 的事件对象 |
返回:
单个 动作 对象、一组 动作 对象或不代表任何 动作 对象的 undefined
。
1 | import { createMachine, actions } from 'xstate'; |
自转换动作
自转换 是当状态转换到自身时,它 可能 退出然后重新进入自身。 自转换可以是 内部 或 外部 转换:
- 内部转换将
不
退出并重新进入自身,因此状态节点的“进入”和“退出”动作将不会再次执行。- 内部转换用
{ internal: true }
表示,或者将target
保留为undefined
。 - 将执行在转换的
actions
属性上定义的 动作。
- 内部转换用
- 外部转换
将
退出并重新进入自身,因此状态节点的entry
和exit
action 将再次执行。- 默认情况下,所有转换都是外部的。 为了明确起见,你可以使用
{ internal: false }
来指示它们。 - 将执行在转换的
actions
属性上定义的 动作。
- 默认情况下,所有转换都是外部的。 为了明确起见,你可以使用
例如,这个计数器状态机,有一个带有内部和外部转换的 'counting'
状态:
1 | onst counterMachine = createMachine({ |
守卫(Guarded)转换
很多时候,你会希望状态之间的转换仅在满足状态(有限或扩展)或事件的某些条件时发生。 例如,假设你正在为搜索表单创建一台状态机,并且你只希望在以下情况下允许搜索:
- 允许用户搜索(本例中为
.canSearch
) - 搜索事件
query
不为空。
这是“守卫转换”的一个很好的用例,这是一种仅在某些条件(cond
)通过时才会发生的转换。 带有条件的转换称为守卫转换。
守卫 (条件函数)
在转换的 .cond
属性上指定的 条件函数(也称为 守卫),作为具有 { type: '...' }
属性的字符串或条件对象 , 并接受 3 个参数:
参数 | 类型 | 描述 |
---|---|---|
context |
object | 状态机 context |
event |
object | 触发条件的事件 |
condMeta |
object | 元数据(见下文) |
condMeta
对象包括以下属性:
cond
- 原始条件对象state
- 转换前的当前状态机状态_event
- SCXML 事件
返回
true
或 false
,决定是否允许进行转换。
1 | const searchValid = (context, event) => { |
单击 EVENTS 选项卡并发送一个类似{ "type": "SEARCH", "query": "something" }
的事件,如下所示:
如果 cond
守卫返回 false
,则不会选择转换,并且不会从该状态节点发生转换。 如果子状态中的所有转换都有判断为 false
的守卫,并阻止它们被选择,则 event
将传播到父状态 并在那里处理。
context
的使用示例:
1 | import { interpret } from 'xstate'; |
提示
通过直接在状态机配置中指定内联的守卫
cond
函数,可以快速构建守卫实现的原型:
1
2
3
4
5
6 // ...
SEARCH: {
target: 'searching',
cond: (context, event) => context.canSearch && event.query && event.query.length > 0
}
// ...在状态机选项的
guards
属性中重构内联 守卫,实现可以更容易地调试、序列化、测试和准确地可视化的守卫。
序列化守卫
守卫 可以(并且应该)被序列化为字符串或具有 { type: '...' }
属性的对象。 守卫的实现细节在状态机选项的guards
属性上指定,其中key
是守卫type
(指定为字符串或对象),值是一个接受三个参数的函数:
context
- 当前状态机 contextevent
- 触发(潜在)转换的事件guardMeta
- 一个包含有关守卫和转换的元数据的对象,包括:cond
- 原始cond
对象state
- 转换前的,当前状态机 state
重构上面的例子:
1 | const searchMachine = createMachine( |
自定义守卫 4.4+
有时,最好不仅序列化 JSON 中的状态转换,还序列化 守卫 逻辑。 这是将守卫序列化为对象的有用之处,因为对象可能包含相关数据:
1 | const searchMachine = createMachine( |
多个守卫
如果你想在某些情况下将单个事件转换到不同的状态,你可以提供一组条件转换。 每个转换都将按顺序进行测试,并且将采用第一个 cond 保护判断为 true 的转换。
例如,你可以建模一扇门,它监听 OPEN 事件,如果你是管理员则进入 ‘opened’ 状态,或者如果 alert 为真 则进入 ‘closed.error’ 状态 ,否则进入 ‘closed.idle’ 状态。
1 | import { createMachine, actions, interpret, assign } from 'xstate'; |
实际应用
vue2
vue3
使用 XState 和 Vue 3 重新创建 iPod 状态机_javascript_EUV-DevPress官方社区 (csdn.net)