At present, whether it is to C business or to B business, the front-end developers have higher and higher requirements, a variety of gorgeous visual effects, complex business logic emerge in an endless stream. In terms of business logic, there is a key point across both back-end business and front-end interactions — state transitions.
Of course, this code implementation itself is not complicated, the real difficulty is how to quickly modify the code.
In the actual development of a project, the ETC principle, namely, Easier To Change, is very important. Why is decoupling good? Why is a single responsibility useful? Why is good naming important? Because these design principles make it easier to change your code. ETC is even the cornerstone of other principles, so to speak, everything we do now is to make it easier to change!! This is especially true for startups.
For example, at the beginning of the project, the current web page had a modal box for editing, and there were two buttons on the modal box, save and cancel. This involves the explicit and implicit state of the modal box and permission management. Over time, requirements and businesses have changed. The current list does not show everything about the project, so in the modal box we need to edit the data as well as present it. At this point we also need to manage the linkage between buttons. This alone can be complex, not to mention involving multiple business entities and subtle controls between multiple roles.
Looking back at our code, despite all the efforts we had made to take advantage of various design principles, trying to quickly and safely change the state changes scattered across various functions was a waste of mind, and it was easy to “slip through the cracks.”
Not only do we need to rely on our own experience to write good code, but we also need some tools.
Finite state machine
A finite state machine is a very useful mathematical model that describes the behavior of a system that can only be in one state at any given time. Of course, only limited and qualitative “patterns” or “states” can be established in the system, and not all (possibly infinite) data related to the system can be described. For example, water can be in one of four states: solid (ice), liquid, gas, or plasma. However, the temperature of water can vary, and its measurement is quantitative and infinite.
To sum up, the three characteristics of finite state machines are:
- The total number of states is finite.
- In one state at any one time.
- Under certain conditions, they transition from one state to another.
In real development, it would also require:
- The initial state
- Events and transition functions that trigger state changes
- Set of final states (possibly no final states)
Let’s start with a simple stoplight transition:
const light = {
currentState: 'green'.transition: function () {
switch (this.currentState) {
case "green":
this.currentState = 'yellow'
break;
case "yellow":
this.currentState = 'red'
break;
case "red":
this.currentState = 'green'
break;
default:
break; }}}Copy the code
Finite state machines have become a popular design pattern in game development. In this way can make each state is independent of the code block, separated from other different state run independently, so it is easy to detect missing condition and remove illegal status, reduces the coupling and improve the robustness of the code, to do so can make debugging more convenient game, but also easier to add new functionality.
For front-end development, we can learn from and recreate experiences that have been used for years in other engineering fields.
XState experience
In fact, developing a simple state machine is not particularly complicated, but it is not easy to have a complete, practical, and visual state machine.
Here I would recommend XState, the library for creating, interpreting, and executing finite state machines and state diagrams.
In short: the above code can be written like this.
import { Machine } from 'xstate'
const lightMachine = Machine({
// Identify id, SCXML ID must be unique
id: 'light'.// Initialization state, green
initial: 'green'.// State definition
states: {
green: {
on: {
// Event name. If TIMRE event is triggered, yellow state is directly changed
TIMRE: 'yellow'}},yellow: {
on: {
TIMER: 'red'}},red: {
on: {
TIMER: 'green'}}}})// Set the current state
const currentState = 'green'
// The result of the conversion
const nextState = lightMachine.transition(currentState, 'TIMER').value
// => 'yellow'
// If the event is passed in undefined, no conversion will occur, and if it is in strict mode, an error will be thrown
lightMachine.transition(currentState, 'UNKNOWN').value
Copy the code
SCXML is the state Graph Extensible Markup Language, and XState follows the standard, so an ID is required. The current state machine can also be converted to JSON or SCXML.
Although transition is a pure function, and it’s very useful, to use a state machine in a real environment, we need something more powerful. Such as:
- Tracking current state
- Executive side effect
- Deal with excessive delays and time
- Communicate with external services
XState provides the interpret function,
import { Machine,interpret } from 'xstate'
/ /... LightMachine code
// The instance of the state machine becomes serivce
const lightService = interpret(lightMachine)
// Events (including initial state) that are triggered when the transition occurs
.onTransition(state= > {
// Returns whether or not the state has changed, and true if the state has changed (or context and action mentioned later)
console.log(state.changed)
console.log(state.value)
})
// Trigger when done
.onDone(() = > {
console.log('done')})/ / open
lightService.start()
// Change the trigger event to send a message, more suitable for the state machine style
// The initialization state is green
lightService.send('TIMER') // yellow
lightService.send('TIMER') // red
// Batch activity
lightService.send([
'TIMER'.'TIMER'
])
/ / stop
lightService.stop()
// Start the current service from a specific state, which is more useful for saving and using the state
lightService.start(previousState)
Copy the code
We can also use it with other libraries in the Vue React framework to achieve the desired functionality with just a few lines of code.
import lightMachine from '.. '
React Hook style
import { useMachine } from '@xstate/react'
function Light() {
const [light, send] = useMachine(lightMachine)
return <>// The current state is green<span>{light.matches('green') && 'green'}</span>// The value of the current state<span>{light.value}</span>// Send a message<button onClick={()= >Send (the 'TIMER')} > switch</button>
</>
}
Copy the code
The current state machine can also be nested to add the action state of the person in the red light state.
import { Machine } from 'xstate';
const pedestrianStates = {
// First state walk
initial: 'walk'.states: {
walk: {
on: {
PED_TIMER: 'wait'}},wait: {
on: {
PED_TIMER: 'stop'}},stop: {}}};const lightMachine = Machine({
id: 'light'.initial: 'green'.states: {
green: {
on: {
TIMER: 'yellow'}},yellow: {
on: {
TIMER: 'red'}},red: {
on: {
TIMER: 'green'
},
...pedestrianStates
}
}
});
const currentState = 'yellow';
const nextState = lightMachine.transition(currentState, 'TIMER').value;
// Return the cascading object
/ / = > {
// red: 'walk'
// }
// You can also write red. Walk
lightMachine.transition('red.walk'.'PED_TIMER').value;
// Return after conversion
/ / = > {
// red: 'wait'
// }
// TIMER can also return the next state
lightMachine.transition({ red: 'stop' }, 'TIMER').value;
// => 'green'
Copy the code
Of course, since we have nested state, we can also use type: ‘parallel’ for serial and parallel processing.
In addition, XState also has extended state context and overguarded Guards. In this way, it can more simulate real life
// Can be edited
functions canEdit(context: any, event: any, { cond }: any) {
console.log(cond)
// => delay: 1000
// Do you have any permissions?
return hasXXXAuthority(context.user)
}
const buttonMachine = Machine({
id: 'buttons'.initial: 'green'.// Extend state, such as user and other global data
context: {
// User data
user: {}},states: {
view: {
on: {
// TIMRE: 'yellow'
// In fact, strings can't express much information
EDIT: {
target: 'edit'.// If you do not have the permission, the conversion is not performed and the original state is kept
// Cond: searchValid if no conditions are attached
cond: {
type: 'searchValid'.delay: 3}},}}}}, {/ / the guards
guards: {
canEdit,
}
})
// XState gives a more appropriate API interface, the Context may not exist at development time
// Or we need to reuse the state machine in different contexts to make the code more extensible
const buttonMachineWithDelay = buttonMachine.withContext({
user: {},
delay: 1000
})
// withContext is a direct substitution, no shallow merge, but we can merge manually
constbuttonMachineWithDelay = buttonMachine.withContext({ ... buttonMachine.context,delay: 1000
})
Copy the code
We can also make transitions through transient states, where the transient state node determines which state the machine should actually enter from the previous state according to conditions. The transient state is represented as an empty string, i.e. ”, as in
const timeOfDayMachine = Machine({
id: 'timeOfDay'.// What is the current state
initial: 'unknown'.context: {
time: undefined
},
states: {
// Transient state
unknown: {
on: {
' ': [{target: 'morning'.cond: 'isBeforeNoon' },
{ target: 'afternoon'.cond: 'isBeforeSix' },
{ target: 'evening'}}},morning: {},
afternoon: {},
evening: {}}}, {guards: {
isBeforeNoon: / /... Check whether the current time is less than noon
isBeforeSix: / /... Check whether the current time is less than 6pm}});const timeOfDayService = interpret(timeOfDayMachine
.withContext({ time: Date.now() }))
.onTransition(state= > console.log(state.value))
.start();
timeOfDayService.state.value
// According to the current time, can be morning afternoon and evening, instead of unknown transition
Copy the code
At this point, I feel that I have covered a lot of XState functionality and there is not enough space to cover all of it, but the current functionality is sufficient for most business needs. If you have other, more complex requirements, refer to the XState documentation.
Here are some features not covered:
- Triggering actions (actions once) and activities (activities continue to trigger until they leave the state)
- Delay events with excessive after
- The service invokes, including Promise, and the two state machines interact with each other
- History state node, which can be configured to save state and roll back state
Of course, in contrast to X-State, there are other state machine tools, such as javascripts -state-machine, Ego, etc. You can consider the use of discretionary.
conclusion
For modern frameworks, whether React Hook or Vue Compoistion Api is in full bloom, their essence is to improve the reuse capability of state logic. However, considering that in most scenarios, state switching itself is subject to specific constraints, good programming habits alone may make it difficult to write code that is depressed. FSM and XState are definitely a weapon.
To encourage the
If you think this article is good, I hope you can give me some encouragement and help me star under my Github blog.
Blog address
reference
XState document
JavaScript and finite state machines