This article mainly investigates the comparison between the use of XState and Redux and introduces the advantages of XState.

preface

The basic principle of Redux is to use a single state tree as the application’s data container (Provider). Components can publish actions to modify the data tree, and updates to the data tree are notified to the component that subscribes to the property. React. CreateContext, React. UseContext, etc. Redux merges the data and status description of the entire application into a state tree and sends changes using actions and reducer, for example:

const appStateTree = {
    state: {... },context: {... }};Copy the code

The basic principle of XState is to design the state diagram of an application by triggering transition from one state of the application to the next. Xstate divides the application into state and context, for example:

const appMachine = {
    initial: 'idle'.context: {... },states: {
        idle: {},
        loading: {},
        loaded: {}}};Copy the code

State represents the state of the application (such as the STATE of the UI), while context represents all the context of the application (such as the data that the application obtains from the background). As you can see, xState clearly provides partitioning at the API level; In Redux, however, this division is not obvious, depending on the user’s definition, and state and context can be intermixed. So why do we divide state and context? Theoretically, two types of data belong to different types. State should drive the application to change its state, while context is provided to the application for data exchange and application. (Of course, although context also drives application change, it is not so much in an interaction as in the data content)

Introduction to Xstate

In simple terms, for example, to develop a list component, we need to fetch the data from the back end and display it. Then this list obviously has the following three states: initial state idle, loading state loading state loaded state. As shown in the state diagram:












xstate.js.org/viz/

const appMachine = Machine({
  // Apply the initial state
  initial: 'idle'.// The context of the application
  context: {
    id: null.theme: 'light'.list: [].timestamp: new Date().getTime(),
    error: null
  },
  Idle, loading, loaded, failure
  states: {
    idle: {
      on: {
        // Listen for LOAD events to transition from idle to loading
        LOAD: {
          target: 'loading'.// Modify the context when this event is triggered
          actions: assign({
            id: (_ctx, evt) = > evt.id
          })
        }
      }
    },
    loading: {
      on: {
        // Listen to the SUCCESS event and transition from loading to loaded
        SUCCESS: {
          target: 'loaded'.// Modify the context when this event is triggered
          actions: assign({
            list: (_ctx, evt) = > evt.listData
          })
        },
        // Listen for FAIL events to transition from loading to failure
        FAIL: {
          target: 'failure'.// Modify the context when this event is triggered
          actions: assign({
            list: (a)= >[].error: (_ctx, evt) = > evt.error
          })
        },
        // Listen for PENDING events to repeatedly trigger the loading state
        PENDING: {
          target: 'loading'.// Modify the context when this event is triggered
          actions: assign({
            timestamp: (a)= > new Date().getTime()
          })
        }
      }
    },
    loaded: {
      // Apply the final state
      type: 'final',},failure: {
      // Apply the final state
      type: 'final',}}});Copy the code

State Transition (States)

An application’s state machine can be used by compiling the state machine into a service and then sending events:

import { interpret } from '@xstate/fsm';
import { appMachine } from './App';
import { getId } from './utils';
// Compile a pure function state machine into a service, and the user expresses side effects
const appService = interpret(appMachine).start();
// Subscription services
appService.subscribe(currentState= > {
  if(currentState.changed) {
    // Listen for state changes, do anything
    console.log(currentState); }});// Modify the application status by sending events
// Before sending LOAD, the service is in idle state. The application state changes from Idle to Loading.
appService.send({ type: 'LOAD'.id: getId() });
// It is already in loading state. Loading state did not register a LOAD event, so nothing was done
appService.send({ type: 'LOAD'.id: getId() });
Copy the code

In the preceding code, the application is in idle state before the LOAD is sent. After the LOAD is sent, the application changes from idle state to Loading state. No matter how many times loading events are sent, the state of the application will not change unless loading events such as SUCCESS, FAIL, or PENDING are sent. Therefore, this is why it is called “state transition” rather than “state modification”, because the application can only perform direct state transition according to the convention of the state graph, which can be understood as only following the path searched by the map software. You can’t space jump with any door (redux does).

Context modification

The context can be modified at will. For example, after successfully entering the loaded state, actions will be triggered to assign the context to @xstate/ FSM.

SUCCESS: {
  target: 'loaded'.// Modify the context when this event is triggered
  actions: assign({
    list: (_ctx, evt) = > evt.listData
  })
}
Copy the code

After the list data is updated, the corresponding component that depends on the list can listen and use the list data to update.

Component communication

Redux’s cross-component communication is well known. What about xState’s capabilities in this regard? This is essentially the same, implemented via React. CreateContext. However, the Machine definition of Xstate itself has a context, and we cannot write two sets of contexts twice. This can be done by wrapping a hook (useAppContext, useMachine code)

Declare the initial context
const AppContext = React.createContext({
  context: {},
  setContext: (a)= >{}});function App() {
  const { context, setCurrentContext } = useAppContext(appMachine.config.context);
  const [state, send, service] = useMachine(appMachine);
  useEffect((a)= > {
    service.subscribe(currentState= > {
      if(currentState.changed) { setCurrentContext(currentState.context); }}); }, [send, service, setCurrentContext]);return (
    <AppContext.Provider value={context}>
       <App />
    </AppContext.Provider>
  );
}
Copy the code

Now that the Provider capability is provided, the corresponding components can be consumed through useContext:

function List(props) {
  const { theme } = props;
  // Use context
  const { list } = useContext(AppContext);
  return (
    <div className={classNames('list'{'theme-night': theme= = ='night',
      'theme-light': theme= = ='light'
    })} >
      {
        list.map(item => <Item key={item.id} {. item} / >)}</div>
  );
}
Copy the code

With useContext, any component has the ability to communicate across components. If a component wants to modify the context, it is generally reasonable to do so through the actions of the state node. If you want to modify the context directly, you can also use setCurrentContext to pass through, but this is not recommended.

contrast

The two are not in conflict per se. The difference between Xstate and Redux is primarily conceptual. In general, the idea of a state machine can be achieved by using enum + switch + useContext statements without xstate. You can also use useContext + useReducer to solve the problem without redux. Both are also used as state management libraries, and the specific differences are as follows:

concept

Xstate xstate.js.org/docs/about/… Redux redux.js.org/introductio… You can understand the concepts by reading the above documents.

The compressed volume

Xstate core package @xstate/ FSM + React useMachine implementation, about 3-5 KB Redux basic about 7 KB.

Maintenance costs

Generally speaking, if the services at the B end do not need to optimize the application volume, the xstate and @xstate/ React packages can be fully introduced for development, with the approximate volume of 40-50KB. For c-terminal services, you can introduce the @xstate/ FSM core package for development, and the useMachine and useAppContext implementations provided above. The estimated volume is 3 to 5kb. However, compared to Redux, the use of Xstate is relatively new and takes a while to learn. However, in terms of maintenance, it has many advantages:

  1. Relatively good expansibility, if properly designed, only need to modify the Machine to expand the state node.
  2. Good mobility. State machines are decoupled from applications to a certain extent, so they can be switched between applications in different components
  3. State machine description of applications can restrict applications to a greater extent, making applications predictable and observable.
  4. Automated tests can be performed using path algorithms (****@****xstate/test)

But it also has obvious disadvantages

  1. Tutorials and best practices are still lacking in China, concepts are unfamiliar and the learning curve is relatively steep (though there is a good official documentation)
  2. At the same time, state and context need to be separately concerned. For application development, more time and effort costs are required (reflected in application design).
  3. The state can’t be extended indefinitely, or the complexity would be too high, so you need to split it properly.

Logical visualization

The Machine declaration for Xstate can be viewed visually at xstate.js.org/viz/. Redux can see the state tree on the console, and the Action trigger. Xstate describes the application through a finite state machine, which is more of a “bird’s eye view,” whereas Redux is more of a “breadcrumb” path. However, XState can also subscribe to state services to complete path tracking, while Redux is more difficult to impose strong constraints on transitions between states.

Code implementation

UseMachine source

import { useState, useEffect, useRef } from 'react';
import { interpret } from '@xstate/fsm';
function useConstant(fn = () = >{{})const ref = useRef();
  if(! ref.current) { ref.current = {v: fn() };
  }
  return ref.current.v;
}
export default function useMachine(machine) {
  const service = useConstant((a)= > interpret(machine).start());
  const [state, setState] = useState(service.state);
  // historyMatches is an extension of service capabilities and is not necessarily required
  service.historyMatches = states= > {
    if(! service.historyStates) {return false;
    }
    const targetValues = states.split('/').reverse();
    const values = service.historyStates.map(state= > state.value).reverse();
    for (let i = 0; i < targetValues.length; i++) {
      if(values[i] ! == targetValues[i]) {return false; }}return true;
  };
  useEffect((a)= > {
    service.historyStates = [state];
    service.subscribe(currentState= > {
      if(currentState.changed) { service.historyStates.push(currentState); setState(currentState); }}); setState(service.state);return (a)= > {
      service.historyStates = [];
      service.stop();
    };
  },
    // eslint-disable-next-line[]);return [state, service.send, service];
}
Copy the code

UseAppContext source

import { useState, useCallback } from 'react';
export default function useAppContext(targetContext) {
  const [context, setContext] = useState(targetContext);
  const setCurrentContext = useCallback(newContext= >{ setContext({ ... context, ... newContext }); }, [context])return { context, setCurrentContext };
}
Copy the code

The App sample

Click github.com/sulirc/xsta… View the sample source code to see it in action.

conclusion

At present, there is no best practice. I feel that although Xstate has many advantages, it will take some time to explore whether it can be used in the project. In general, xstate can do what Redux can do (though it needs to be manually extended). What Xstate can do, Redux can’t.