DEV Community

Felix Guerin
Felix Guerin

Posted on • Updated on

Replacing Vuex with XState

I have been learning XState and state machines these past few months and I absolutely love it. I decided to build an app using Vue and XState. I wanted to share this with you all, as I have seen a lot of posts, videos, tutorials, etc. on how to integrate XState with React, but not a lot for Vue.

At first, I used Vuex in conjonction with XState. I was inspired a lot by this article from Phillip Parker, as well as this Github repo by the same author. I highly suggest you read the article and check out the code if you haven't already, I learned a lot from it.

Basically, for every feature of the app, I had a Vuex module and a corresponding XState state machine. It was working well but I knew I wasn't using XState to its full potential.

After more research, I found a way of completely getting rid of Vuex and still have a global reactive state that leverages Vue's capabilities, along with sturdy Finite State Machines that make all the features that XState provides available. It more closely resembles the Vue recipe shown in XState's docs.

Instead of Vuex, I use an event bus pattern to manage global state. This means creating a new Vue instance and passing it whatever I want to share between my components. It could be only one instance for a simple app, but most apps will probably benefit from breaking it down into multiple modules (like with Vuex).

Then you can simply pass in this Vue instance what you need from your XState machine. I wrote a function that returns a Vue instance that exposes the machine's state, context and send() method and reacts to changes in the machine.

import Vue from "vue";
import { interpret } from "xstate";

export const generateVueMachine = machine => {
    return new Vue({
        created() {
            this.service
                .onTransition(state => {
                    this.current = state;
                    this.context = state.context;
                    if (process.env.NODE_ENV === "development") {
                        console.log(`[ ${machine.id.toUpperCase()} STATE ]`, this.current.value);
                    }
                })
                .start();
        },
        data() {
            return {
                current: machine.initialState,
                context: machine.context,
                service: interpret(machine)
            };
        },
        methods: {
            send(event) {
                this.service.send(event);
            }
        }
    });
};

You can then write a new file, for exemple fetchMachine.js, where you create an XState machine. You use the generateVueMachine() function and pass it your state machine as an argument, which returns a Vue instance that you can export.

import { Machine, assign } from "xstate";
import { generateVueMachine } from "./generateVueMachine";

const machine = Machine({ /*...machine config */ });

export const fetchMachine = generateVueMachine(machine);

Now I can reference this machine wherever I want in my app and react to its changes by using Vue's computed properties.

<template>
    <button @click="onFetch" v-if="!fetchState.matches('fetching')">Fetch<button>
    <p>{{ fetchContext.fetchResult }}</p>
</template>

<script>
// fsm
import { fetchMachine } from "./fsm/fetchMachine";

export default {
    computed: {
        fetchState() {
            return fetchMachine.current;
        },
        fetchContext() {
            return fetchMachine.context;
        }
    },
    methods: {
        onFetch() {
            fetchMachine.send({type: 'FETCH'});
        }
    }
};
</script>

And that's it.

Here is a link to my app's repo so you can see how I applied this in a real context (the state machine files are in client/fsm).

I would really appreciate any feedback on what can be done better here.

EDIT:
I created a Vue plugin on npm to facilitate this setup an remove some of the boiler plate. You can find it at https://github.com/f-elix/vue-xstate-plugin.

Top comments (12)

Collapse
 
danroc profile image
Daniel da Rocha

Thanks for this Felix. I went on a similar path to you: after getting excited about all the possibilities of xstate, I started using it with Vuex as written by Parker. But I also felt things were still a bit messy and verbose, while some of the guides on xstate's docs show a much nicer implementation.

I was messing around with Nuxt plugins in my case and will give your approach a try. xstate shows so much potential and I can't wait until Vue3 comes out and we can use @xstate/vue with it!

Collapse
 
felix profile image
Felix Guerin

Yes! Vue 3 will definitely make things easier. What Nuxt plugins did you try?

Collapse
 
danroc profile image
Daniel da Rocha

I was actually trying to use apollo to fetch data in my machines and for that I needed to reference the app somehow. So I figured I'd do it within a plugin file, so that I could access app:

// fetchMachine plugin
const fetchMachine = (app) => {
  return Machine({
      // ...
  })
}

export default ({ app }, inject) => {
  inject('fetchMachine', fetchMachine(app))
}
// my-component.vue
<script>
import { interpret } from 'xstate'
export default {
  data() {
    return {
      fetchService: interpret(this.$fetchMachine),
      current: this.$fetchMachine.initialState
    }
  },
  // ...
  created() {
    this.fetchService.onTransition((state) => (this.current = state)).start()
  },
  methods: {
    send(event) {
      this.fetchService.send(event)
    },
    matches(value) {
      return this.current.matches(value)
    }
  }
}

The component part above is taken from the xstate docs, the Vue implementation of their reddit API machine.

But this is very specific to one machine, and that's why I like your method because I can create machines on the fly as needed...

Thread Thread
 
felix profile image
Felix Guerin

I hadn't seen that. I'll definitely look at it. Making the machines available throughout the whole app as plugins is a really good idea. No need to import it every time.

Thread Thread
 
felix profile image
Felix Guerin

Hey Daniel, I just published a Vue plugin to facilitate the setup outlined in the post. The link is in the edit at the end if you're interested!

Thread Thread
 
danroc profile image
Daniel da Rocha

that's really cool Felix! It makes life easier and hopefully inspires othe people to try xstate and fsm in their projects!

Collapse
 
danroc profile image
Daniel da Rocha

One general question:

How do you deal with notifications? Let's say you have a fetch action which then transitions to either a 'sucess' or a 'failed' state. As in the docs, if you are using invoke, you could do something like:

      onError: {
          target: 'failure',
          actions: assign({
            errorMessage: (context, event) => {
              // event is:
              // { type: 'error.execution', data: 'No query specified' }
              return event.data;
            }
          })
        },

Would you then have a watcher on your frontend checking the value of errorMessage, or would you just watch for the 'failure' state and react accordingly?

Or something else completely?

Collapse
 
felix profile image
Felix Guerin • Edited

I guess both approaches are valid.

In the app I mention in the post I just assign the error to a context property. The component then accesses it via a computed property and an error message appears when there is a value.

You could also conditionally show the error message depending on the machine's state (success, error, etc.), which is what I did initially. In my case though, the error state was identical to the idle state, just with an error message shown. I didn't see a reason to have an error state then.

I'm still unsure if the approach I took is valid in the world of statecharts but it works!

Collapse
 
mimischi profile image
Michael Gecht

Thanks for the post Felix! I was planning to integrate xstate into my App, following this recent post: dev.to/davidkpiano/no-disabling-a-...

I am quite happy with Vuex and would like to integrate xstate to follow Davids advice on using FSM to handle the UI state in a sane way. Do you think it is reasonable to use your approach to FSM in combination with Vuex? I was thinking of triggering already available Vuex actions from the FSM actions.

Collapse
 
felix profile image
Felix Guerin

Sure, I don't see why you couldn't do that. I also like Vuex a lot, but the goal for me was to not have 2 libraries for managing state in an app and reduce my bundle size as much as possible.

I fail to see how it would be useful to have your actions in a Vuex store instead of your machine config though.

Collapse
 
mimischi profile image
Michael Gecht • Edited

I fail to see how it would be useful to have your actions in a Vuex store instead of your machine config though.

It's just inertia. I currently use Vuex actions as a central place for API calls. Putting the actions into FSM would lead to some refactoring -- which is surely doable -- but I'd rather stick with the my approach if possible.

Thread Thread
 
felix profile image
Felix Guerin

Ok I see. I think you could just import your store in your fsm file and dispatch the actions from your machine.