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)
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!
Yes! Vue 3 will definitely make things easier. What Nuxt plugins did you try?
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 accessapp
: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...
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.
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!
that's really cool Felix! It makes life easier and hopefully inspires othe people to try xstate and fsm in their projects!
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: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?
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!
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.
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.
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.
Ok I see. I think you could just import your store in your fsm file and dispatch the actions from your machine.