Usando XSTATE para VueJS





Un pequeño ejemplo del uso de la biblioteca XState de David Khourshid para describir declarativamente la lógica de un componente de VueJS 2. XState es una biblioteca muy avanzada para crear y usar máquinas de estado en JS. No es una mala ayuda en la difícil tarea de crear aplicaciones web.



Prehistoria



En mi último artículo, describí brevemente por qué se necesitan las máquinas de estado (máquinas de estado) y una implementación simple para trabajar con Vue. Mi bicicleta solo tenía estados y la declaración estatal se veía así:



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


De hecho, se trataba de una enumeración de estados, y para cada uno de ellos se describió una serie de posibles estados a los que puede ir el sistema. La aplicación simplemente "dice" a la máquina de estado: quiero entrar en ese estado, si es posible, la máquina entra en el estado deseado.



Este enfoque funciona, pero existen inconvenientes. Por ejemplo, si un botón en un estado diferente debe iniciar una transición a diferentes estados. Tendremos que cercar las condiciones. En lugar de ser declarativos, hacemos un lío.



Habiendo estudiado la teoría en videos de YouTube, quedó claro que los eventos son necesarios e importantes. Este tipo de declaración nació en mi cabeza:



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


Y esto ya es muy similar a cómo describe los estados la biblioteca XState. Después de leer el muelle con más atención, decidí poner mi bicicleta casera en el granero y cambiarme a una de marca.



VUE + XState



La instalación es muy simple, lea el documento, después de la instalación incluimos XState en el componente:



import {Machine, interpret} from ‘xstate’


Creamos un automóvil basado en el objeto de declaración:



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'
          }
        }
    }
})


Está claro que hay estados 'inactivo', '' esperando Confirmación '... y hay eventos en mayúsculas GET, CANCEL, CONFIRM….



La máquina en sí no funciona, debe crear un servicio a partir de ella utilizando la función interpretar. Colocaremos un enlace a este servicio en nuestro estado, y al mismo tiempo un enlace al estado actual:



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


El servicio debe iniciarse - start (), y también indicar que cuando el estado cambia, actualizamos el valor de current:



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


Agregamos la función de envío a los métodos y la usamos para controlar la máquina, para enviarle eventos:



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


Bueno, entonces todo es simple. Envía un evento simplemente llamando a:



this.send(‘SUCCESS’)


Descubra el estado actual:



this.current.value


Compruebe si la máquina está en determinadas condiciones de la siguiente manera:



this.current.matches(‘waitingData')




Poniendolo todo junto:



Modelo
<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)
    }
  })
}




Y, por supuesto, todo esto se puede tocar en jsfiddle.net



Visualizador



XState proporciona una gran herramienta, el Visualizador . Puede ver el diagrama de su automóvil en particular. Y no solo para mirar, sino también para hacer clic en eventos y realizar transiciones. Así es como se ve nuestro ejemplo:







Salir



XState funciona muy bien con VueJS. Esto simplifica el trabajo del componente y le permite deshacerse del código innecesario. Lo principal es que la declaración de la máquina le permite comprender rápidamente la lógica. Este ejemplo es simple, pero ya lo probé en un ejemplo más complejo para un proyecto de trabajo. El vuelo es normal.



En este artículo, utilicé solo la funcionalidad más básica de la biblioteca, ya que todavía tengo suficiente, pero la biblioteca contiene muchas más características interesantes:



  • Transiciones vigiladas
  • Acciones (entrada, salida, transición)
  • Estado extendido (contexto)
  • Estados ortogonales (paralelos)
  • Estados jerárquicos (anidados)
  • Historia


Y también hay bibliotecas similares, por ejemplo Robot. Aquí hay una comparación de las máquinas de estado de comparación: XState vs. Robot . Entonces, si está interesado en un tema, tendrá algo que hacer.



All Articles