At present, whether it is to C business or to B business, the requirements for front-end developers are getting higher and higher, and various gorgeous visual effects and complex business logic emerge in an endless stream. As far as business logic is concerned, there is a key point that runs through both back-end business and front-end interactions — state transition.

Of course, the implementation of this code itself is not complicated, the real difficulty is how to quickly modify the code.

In the actual process of developing a project, the ETC principle, 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 your code more susceptible to change. 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 has a modal box for editing, and two buttons on the modal box for save and cancel. This involves the explicit and implicit state of the modal frame and permission management. Over time, the requirements and the business have changed. The current list does not show everything about the project, and in the modal box we need to not only edit the data, but also present the data. We also need to manage the interaction between the buttons. This alone is complex, not to mention involving multiple business entities and subtle controls between multiple roles.

Taking a look at our own code, although we’ve done a lot of work to take advantage of various design principles, trying to quickly and safely change state changes scattered across functions can be very mind-boggling and easy to “slip through the net.”

At this point, we not only need to rely on their own experience to write code, but also need some tools to help.

Finite state machine

A finite state machine is a very useful mathematical calculation model that describes the behavior of a system that can only be in one state at any given time. Of course, only a limited, qualitative “pattern” or “state” can be established in the system, and it does not describe all the (possibly infinite) data related to the system. 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, there are three characteristics of a finite state machine:

  • The total number of states is finite.
  • Being in one state at any one time.
  • Under certain conditions, there is a transition from one state to another.

In practical development, it also requires:

  • The initial state
  • Events and transition functions that trigger state changes
  • The set of final states (possibly no final state)

Let’s start with a simple traffic light state 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; }}}

Finite state machines are so prominent in game development that they have become a common design pattern. 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 and re-invent what we’ve learned from years of experience in other engineering areas.

XState experience

In fact, developing a simple state machine is not particularly complicated, but it is not easy to create a perfect, practical, and visualized state machine.

Here I recommend Xstate, which is a library for creating, interpreting, and executing finite state machines and state graphs.

In short: the above code can be written like this.

Import {Machine} from 'xstate' const lightMachine = Machine({// ID must be unique SCXML ID: 'light', // initial state, green initial: 'green', // state definition states: {green: {on: {// event name, if a TIMRE event is triggered, go directly to yellow TIMRE state: 'yellow' } }, yellow: { on: { TIMER: 'red' } }, red: { on: { TIMER: 'green'}}}}) // Set currentState const currentState = 'green' // Result of conversion const nextState = lightMachine. Transition (currentState,) 'TIMER').value // => 'yellow' // If the event passed in is undefined, the conversion will not occur. If it is in strict mode, Throw an error LightMachine. Transition (currentState, 'UNKNOWN').value

Where SCXML is the Extensible Markup Language for State Graph, Xstate follows the standard, so you need to provide an ID. The current state machine can also be converted to JSON or SCXML.

Although Transition is a pure function, it is very useful, but to use the state machine in the real world, we need more power. Such as:

  • Track current state
  • Executive side effect
  • Deal with excessive delays as well as time
  • Communicate with external services

Xstate provides the interpret function,

Import {Machine,interpret} from 'xstate' //... // The instance of the state machine becomes serivce const lightService = Interpret (lightMachine) // When converting, .onTransition(state => {// Returns whether or not the transition has changed. If the state has changed (or if the context and action have changed), Returns true console.log(state.changed) console.log(state.value)}) // OnDone (() => {console.log('done')}) // Open Lightservice.start () // Change the trigger event to send a message, Send ('TIMER') // yellow lightservice. send('TIMER') // red // Batch activity lightService.send([ 'TIMER', 'Timer']) // Stop LightService.stop () // Start the current service from a specific state. This is more useful for saving state and using LightService.start (previousState)

We can also combine other libraries with the Vue React framework to achieve the functionality we want 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 <> // State <span>{light.matches('green') &&' green'}</span> // < span > {light. Value} < / span > / / send a message < button onClick = {() = > send (the 'TIMER')} > switch < / button > < / a >}

The current state machine can also be nested, adding the person’s action state at the red light.

import { Machine } from 'xstate'; Const pedestrianStates = {// Initial: 'walk', states: {walk: {on: {PED_TIMER: 'wait'}}, wait: {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; } // transition({red: 'stop'}, 'TIMER').value;} // return lightMachine. // => 'green'

Of course, since we have nested state, we can also use type: ‘parallel’ for serial and parallel processing.

In addition, Xstate has extended state context and overguard guards. In this way, it can simulate real life more

Function 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: Context: {// User: {}}, States: {view: {on: {// Timre: {// Timre: {// Timre: 'yellow' // In fact the string does not express much information, the object is required to represent EDIT: {target: 'EDIT ', // If you do not have this permission, no conversion, in the original state // If no conditions are attached, direct cond: SearchValid cond: {type: 'searchValid', delay: 3}},}}}}, {// guard: {canEdit,}}) // Xstate gives a more appropriate API, the Context may not exist at development time // or we need to reuse the state machine in a different Context Context, This code extensibility stronger const buttonMachineWithDelay = buttonMachine. WithContext ({user: {}, delay: 1000}) / / withContext is directly replace, not to shallow, but we can manually merge const buttonMachineWithDelay = buttonMachine. WithContext ({... buttonMachine.context, delay: 1000 })

We can also transition through transient states, where the transient state node can determine, depending on the conditions, which state the machine should actually enter from the previous state. The transient state is represented by an empty string, i.e. ‘, as in

Const timeOfDayMachine = Machine({id: 'timeOfDay', 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: //... Verify that the current time is less than noon isBeforeSix: //... Confirm whether the current time is less than 6:00 PM}}); const timeOfDayService = interpret(timeOfDayMachine .withContext({ time: Date.now() })) .onTransition(state => console.log(state.value)) .start(); TimeOfDayService. State. The value / / according to the current time, can be a morning afternoon and evening, instead of unknown state

At this point, I think I’ve covered a lot of the functionality of Xstate, and I don’t have enough space to cover all of it, but the current functionality is sufficient for most business needs. For more complex requirements, refer to the Xstate documentation.

Here are some features not covered:

  • Entering and leaving a state triggers actions (once) and activities (activities continue to trigger until leaving a state).
  • Delayed events with excessive after
  • The service calls Invoke, including the Promise, and the interaction between the two state machines
  • Historical state node, which can be configured to save state and reverse state

Of course, there are other state machine tools like Javascript – State – Machine, Ego, etc. You can consider using it at your discretion.

conclusion

For modern frameworks, both the React Hook and the Vue Compoistion API are designed to improve the reusability of state logic. However, considering that in most scenarios, the switching of the state itself is subject to certain constraints, if only by good programming habits, I am afraid that it is difficult to write code with depressed modifications. FSM and Xstate are the obvious tools.

To encourage the

If you think this article is good, I hope you can give me some encouragement and help star it under my GitHub blog.

Blog address

reference

XState document

JavaScript with finite state machines