This time, ycaptain will unlock a new article in the series “XState finite state machines and state diagrams”.

XState? What? Another state management library?

Some readers may feel PTSD after seeing this and cry “I can’t learn “~

Don’t worry, let me explain it in detail.

State management is a real headache

When we write an application, we’re really using apis to control the performance of the application. Good apis generally have three features:

  • Self-explanatory: They are well commented or self-explanatory, and you can clearly understand what the API is doing just by reading the documentation
  • Predictable: They should be predictable, and the result should be the same every time they are executed under the same conditions
  • Testable: They should be testable, and you can test them by using the MOCks or test sets provided by the API to ensure that they perform as expected

What about the apis that most people write? Most people write apis that also have three features

When people use our apps, they don’t always use them the way we expect them to. Let’s assume that there is an ideal user who actually uses the application the way we want.

Take a network request for example. In this example, we will send a network request and display the result of the request in the application.

onSearch(query) {
    fetch(BD_API + '&tags=' + query)
        .then(
            data= > {
                this.setState({ data }); }); }Copy the code

This code looks simple and easy to complete.

Next, let’s assume that there is a performance problem on the back end, or that some time-consuming operation is required, and the search API may return results in seconds or even tens of seconds, so we need to add a loading state.

onSearch(query) {
    this.setState({ loading: true });

    fetch(BD_API + '&tags=' + query)
        .then(
            data= > {
                this.setState({ data, loading: false}); }); }Copy the code

Just like that. We need to display the loading interface before obtaining data. After obtaining data, set loading to false, hide the loading interface, and display the obtained results.

So are we done now? Don’t. What if something goes wrong in the request? We must hide the loading screen to display error messages.

onSearch(query) {
    this.setState({ loading: true });

    fetch(BD_API + '&tags=' + query)
        .then(
            data= > {
                this.setState({ data, loading: false });
            }
        ).catch(error= > {
            this.setState({
                loading: false.error: true
            });
        });
}
Copy the code

Are we bug-free now?

Don’t. We also need to make sure that the error status is cleared when the user makes the request again.

onSearch(query) {
    this.setState({
        loading: true.error: false
    });

    fetch(BD_API + '&tags=' + query)
        .then(
            data= > {
                this.setState({
                    data,
                    loading: false.error: false
                });
            }
        ).catch(error= > {
            this.setState({
                loading: false.error: true
            });
        });
}
Copy the code

As you can see, as the complexity of applications increases, so does our mental burden. I’m sure you can imagine some of the actual scenarios at this point.

So, if the PM adds requirements at this point, do we now need to provide the ability to cancel requests?

As previously assumed, this request takes too long and the user may issue another request to replace it.

onSearch(query) {
    if (this.state.loading) {
        return;
    }

    this.setState({
        loading: true.error: false.canceled: false
    });

    fetch(BD_API + '&tags=' + query)
        .then(
            data= > {
                if (this.state.canceled) {
                    return;
                }

                this.setState({
                    data,
                    loading: false.error: false
                });
            }
        ).catch(error= > {
            if (this.state.canceled) {
                return;
            }

            this.setState({
                loading: false.error: true
            });
        });
}

onCancel() {
    this.setState({
        loading: false.error: false.canceled: true
    });
}
Copy the code

Our code gets more complex at this point, and we’re handling a lot of logic in such a small search event.

You’ve probably heard of Spaghetti Code, which is Code that relies on each other’s logic and is very difficult to maintain.

Some people might say, “I don’t write Spaghetti Code. My Code is modular, layered, high cohesion, low coupling.”

Even so, most people fall into another trap: Lasagna Code, where modules of Code are coupled to each other in addition to snippets of Code.

To solve this problem, we can approach logic in a bottom-up fashion.

In this mode, all the logic for handling either onClick or onChange events is under the event.

Each event may correspond to many different actions, and some of them modify state.

However, you must be careful to select the right action for the event, or you can change the state in the wrong way, such as writing a series of if else or switch statements inside the action.

Such code quickly becomes unmaintainable. Because all the logic only exists in your head, when you write the test, you have to retrieve it from deep in memory and interpret it.

If you try to explain this logic to new team members, for example, you will find it difficult to get them to understand this logic, let alone an entire project.

This also makes the code more difficult to extend, as when we introduced the cancel feature, it was much harder to add than the previous feature point. New features, such as “cancel requests,” make the code exponentially more difficult to maintain.

Let’s think about it another way.

When we need to implement a set of components that depend on each other. We’ll use a framework that separates components, such as React, to implement them. These components can be directly embedded anywhere on the page.

In design, they are logically separated from each other and are related through props. But in a real world scenario, different components are not independent. We need to organize nesting, creation, modification, and communication between components.

So, what’s our solution?

Solution: Finite state machines and state diagrams

Many of you may have learned about state machines in school and academic definitions, and academic definitions may be expensive to understand, so let’s use an example to give you an intuition.

Finite state machines have five important parts

  • Initial State
  • A limited set of states
  • A limited set of events
  • A set of state transitions driven by events
  • A limited set of final States

For example, when a fetch returns a Promise, it goes into a pending state. If it is resolved, it enters the fullfilled state. If it is rejected, it enters the Rejected state.

For application development, most states are continuous. Relatively speaking, the proportion of the final state will be much smaller. In the Promise, this is the final state, which is fulfilled and Rejected.

Search based on finite state machine

Going back to the previous search problem, we can model it using finite state machines.

The default state is IDLE. When the search event is triggered, the application goes to searching.

If the search event is triggered in searching state, the application is still in searching state.

Next, we can resolve or reject the results of the search and enter the state of success or failure, respectively.

In these two states, we can launch a new search event again, through the arrow pointing, we can clearly see that it will go back to searching state.

The state machine logic above can be written as a JSON object (JSON is perhaps more readable than the black box function, which enumerates all possible States, Actions, and Transitions in a simple way).

const machine = {
    initial: 'idle'.states: {
        idle: {
            on: { SEARCH: 'searching'}},searching: {
            on: {
                RESOLVE: 'success'.REJECT: 'failure'.SEARCH: 'searching'}},success: {
            on: { SEARCH: 'searching'}},failure: {
            on: { SEARCH: 'searching'}}}};function transition(state, event) {
    return machine.states[state].on[event];
}
Copy the code

IO /s/xstate-se…

Implement Live Share based on finite state machine

As an example, the author of XState himself, David from Microsoft, has also developed VSCode’s Live Share plug-in, which can be used for pairing, interviewing, or code sharing.

David wrote a lot of bugs when developing this plug-in because of the complicated logic. Especially in this kind of tool application, we need to stay on the same page and deal with a lot of states.

Take login for example. After logging In to a Signed In state, you can do two things: share a session or join a session. Logon failed, need to return Signed Out status.

That’s the basic process. In addition, a user may sign out during a share session or attempt to join another session while the share session is in progress. This logic can be categorized as a bunch of if else’s, but with a state machine you can make it self-explanatory.

Moreover, by monitoring state transfer, the author easily realized the requirement of buried points added by Live Share later.

transition(currentState, event) {
    const nextState = / /...

    Telemetry.sendEvent(
        currentState,
        nextState,
        event
    );

    return nextState;
}
Copy the code

It is obvious from the graph that users sign in the most frequently.

Once logged in, users share and join sessions with similar frequency. It also clearly shows how many users entered the SUCCESS state and how many entered the error state.

XState

XState is the JS implementation of finite State Machine and State graph for modern Web development. XState does not create new concepts by itself. Instead, the IMPLEMENTATION of SCXML (State Chart Extensible Markup Language) follows W3C.

So far, there are 17K stars on GitHub.

XState has good ecological support, including

  • Xstate: core library + interpreter for finite state machines and state diagrams
  • Xstate/FSM: Minimal finite state library
  • Xstate /graph: Graph traversal tool
  • Xstate/React: Hooks and utilities for react applications
  • Xstate /vue: Composition functions and utilities for VUE applications
  • Xstate /test: Model-based testing tools
  • Xstate /inspect: Visual library

, etc.

In the next section, we’ll talk about how visualization tools can be used to reduce the mental burden of development and increase productivity.

XState official documentation: xstate.js.org/docs/guides…

Can’t wait want to directly to fit the friend can have a look at the official Todo MVC sample: xstate.js.org/docs/exampl…

PS: The team is hiring!