Using XSTATE for VueJS





A small example of using the XState library by David Khourshid to declaratively describe the logic of a VueJS 2 component. XState is a very advanced library for creating and using state machines in JS. Not a bad help in the difficult task of creating web applications.



Prehistory



In my last article, I briefly described why state machines (state machines) are needed and a simple implementation for working with Vue. My bike only had states and the state declaration looked like this:



{
    idle: ['waitingConfirmation'],
    waitingConfirmation: ['idle','waitingData'],
    waitingData: ['dataReady', 'dataProblem'],
    dataReady: [‘idle’],
    dataProblem: ['idle']
}


In fact, it was an enumeration of states, and for each an array of possible states that the system can go to was described. The application simply “says” to the state machine - I want to go into such a state, if possible, the machine goes into the desired state.



This approach works, but there are inconveniences. For example, if a button in a different state should initiate a transition to different states. We'll have to fence in the conditions. Instead of being declarative, we get a mess.



Having studied the theory on videos from YouTube, it became clear that events are necessary and important. This kind of declaration was born in my head:



{
  idle: {
    GET: 'waitingConfirmation',
  },
  waitingConfirmation: {
    CANCEL: 'idle',
    CONFIRM: 'waitingData'
  },
  waitingData: {
    SUCCESS: 'dataReady',
    FAILURE: 'dataProblem'
  },
  dataReady: {
    REPEAT: 'idle'
  },
  dataProblem: {
    REPEAT: 'idle'
  }
}


And this is already very similar to how the XState library describes states. After reading the dock more carefully, I decided to put my homemade bicycle in the barn and switch to a branded one.



VUE + XState



Installation is very simple, read the doc, after installation we include XState in the component:



import {Machine, interpret} from ‘xstate’


We create a car based on the declaration object:



const myMachine = Machine({
    id: 'myMachineID',
    context: {
      /* some data */
    },
    initial: 'idle',
    states: {
        idle: {
          on: {
            GET: 'waitingConfirmation',
          }
        },
        waitingConfirmation: {
          on: {
            CANCEL: 'idle',
            CONFIRM: 'waitingData'
          }
        },
        waitingData: {
          on: {
            SUCCESS: 'dataReady',
            FAILURE: 'dataProblem'
          },
        },
        dataReady: {
          on: {
            REPEAT: 'idle'
          }
        },
        dataProblem: {
          on: {
            REPEAT: 'idle'
          }
        }
    }
})


It is clear that there are states' idle ',' 'waitingConfirmation' ... and there are events in uppercase GET, CANCEL, CONFIRM….



The machine itself does not work, you need to create a service from it using the interpret function. We will place a link to this service in our state, and at the same time a link to the current state:



data: {
    toggleService: interpret(myMachine),
    current: myMachine.initialState,
}


The service must be started - start (), and also indicate that when the state transitions, we update the value of current:



mounted() {
    this.toggleService
        .onTransition(state => {
            this.current = state
         })
        .start();
    }


We add the send function to the methods, and use it to control the machine - to send events to it:



methods: {
   send(event) {
      this.toggleService.send(event);
   },
} 


Well, then everything is simple. Send an event simply by calling:



this.send(‘SUCCESS’)


Find out the current state:



this.current.value


Check if the machine is in a certain condition as follows:



this.current.matches(‘waitingData')




Putting it all together:



Template
<div id="app">
  <h2>XState machine with Vue</h2>
  <div class="panel">
    <div v-if="current.matches('idle')">
      <button @click="send('GET')">
        <span>Get data</span>
      </button>
    </div>
    <div v-if="current.matches('waitingConfirmation')">
      <button @click="send('CANCEL')">
        <span>Cancel</span>
      </button>
      <button @click="getData">
        <span>Confirm get data</span>
      </button>
    </div>
    <div v-if="current.matches('waitingData')" class="blink_me">
      loading ...
    </div>
    <div v-if="current.matches('dataReady')">
      <div class='data-hoder'>
        {{ text }}
      </div>
      <div>
        <button @click="send('REPEAT')">
          <span>Back</span>
        </button>
      </div>
    </div>
    <div v-if="current.matches('dataProblem')">
      <div class='data-hoder'>
        Data error!
      </div>
      <div>
        <button @click="send('REPEAT')">
          <span>Back</span>
        </button>
      </div>
    </div>
  </div>
  <div class="state">
    Current state: <span class="state-value">{{ current.value }}</span>
  </div>
</div>




Js
const { Machine, interpret } = XState

const myMachine = Machine({
    id: 'myMachineID',
    context: {
      /* some data */
    },
    initial: 'idle',
    states: {
        idle: {
          on: {
            GET: 'waitingConfirmation',
          }
        },
        waitingConfirmation: {
          on: {
            CANCEL: 'idle',
            CONFIRM: 'waitingData'
          }
        },
        waitingData: {
          on: {
            SUCCESS: 'dataReady',
            FAILURE: 'dataProblem'
          },
        },
        dataReady: {
          on: {
            REPEAT: 'idle'
          }
        },
        dataProblem: {
          on: {
            REPEAT: 'idle'
          }
        }
    }
	})



new Vue({
  el: "#app",
  data: {
  	text: '',
  	toggleService: interpret(myMachine),
    current: myMachine.initialState,
  },
  computed: {

  },
  mounted() {
    this.toggleService
        .onTransition(state => {
          this.current = state
        })
        .start();
  },
  methods: {
    send(event) {
      this.toggleService.send(event);
    },
    getData() {
      this.send('CONFIRM')
    	requestMock()
      .then((data) => {       
      	this.text = data.text   
      	this.send('SUCCESS')
      })
      .catch(() => this.send('FAILURE'))
    },

  }
})

function randomInteger(min, max) {
  let rand = min + Math.random() * (max + 1 - min)
  return Math.floor(rand);
}

function requestMock() {
  return new Promise((resolve, reject) => {
  	const randomValue = randomInteger(1,2)
  	if(randomValue === 2) {
    	let data = { text: 'Data received!!!'}
      setTimeout(resolve, 3000, data)
    }
    else {
    	setTimeout(reject, 3000)
    }
  })
}




And of course all this can be touched on jsfiddle.net



Visualizer



XState provides a great tool, the Visualizer . You can see the diagram of your particular car. And not only to look but also to click on events and make transitions. This is how our example looks like:







Outcome



XState works great with VueJS. This simplifies the work of the component and allows you to get rid of unnecessary code. The main thing is that the declaration of the machine allows you to quickly understand the logic. This example is simple, but I have already tried it on a more complex example for a working project. The flight is normal.



In this article, I used only the most basic functionality of the library, since I still have enough of it, but the library contains many more interesting features:



  • Guarded transitions
  • Actions (entry, exit, transition)
  • Extended state (context)
  • Orthogonal (parallel) states
  • Hierarchical (nested) states
  • History


And there are also similar libraries, for example Robot. Here's a comparison of Comparing state machines: XState vs. Robot . So if you are interested in a topic, you will have something to do.



All Articles