DEV Community

Cover image for Power of state machine and reactive store combined in Vue3
bkamkl9
bkamkl9

Posted on • Edited on

Power of state machine and reactive store combined in Vue3

Introduction

The xMachineVue library combines concept of a state machine with a reactive store. By combining these two concepts, it allows you to manage your application’s state in a structured and predictable way. It provides you with the ability to define different states and transitions between them, while also allowing you to reactively track changes to your application’s state. This makes it easier to manage complex state logic in Vue.js applications.

Full library documentation: https://kamilbartczak03.github.io/XMachineVue/pages/quick-start.html

Creating a new machine

To create a new state machine, import the createMachine function from xmachinevue. In the first argument, specify the ID and in the second, define the body of the machine.

Machine structure

The body of the machine is an object with two required keys: "states" and "initial".

States, as the name suggests, are used for defining states. The states are objects that contain functions called actions. It's very similar to the Pinia syntax. The initial is a string for specifying which state should be loaded initially.

import { createMachine } from 'xmachinevue'

const machine =  createMachine('agreement', {
  initial: "INITIAL",
  state: {
    INITIAL: { /* (...) */ },
    AGREED: { /* (...) */ }
  }
}) 
Enter fullscreen mode Exit fullscreen mode

Under machine.current.value is a reactive ref with current machine state can be accessed. In the above example, the value is "INITIAL"

State transitions

There are two ways of transitioning to a different state. We can directly call changeState('STATE_KEY') on machine or use $changeState('STATE_KEY') from action context.

machine.changeState('AGREED')
machine.current.value // "AGREED"
Enter fullscreen mode Exit fullscreen mode

Actions

Actions are methods defined in the states object. They are functions with a special this context and can accept any arguments. Actions allow you to define behaviors that should be executed when a certain state is active or when transitioning from one state to another. This could include anything from changing the state of the machine, dispatching other actions, or even side effects like making an API call or logging a message.

import { createMachine } from 'xmachinevue'

const machine =  createMachine('counter-machine', {
  initial: "INITIAL",
  state: {
    INITIAL: {
      handleCheckboxClick(value: boolean) { /* (...) */}
      showConfirmModal() { /* (...) */ }
    },
    AGREED: { /* (...) */ }
  }
}) 
Enter fullscreen mode Exit fullscreen mode

Dispatching actions

You can dispatch only actions from certain active states. For example, if the current active state is 'INITIAL' you can dispatch only actions from this state, and you can do it in two ways.

Directly on the machine:

machine.INITIAL.handleCheckboxClick(true)
Enter fullscreen mode Exit fullscreen mode

or using this context of other action:

INITIAL: {
  handleCheckboxClick(value: boolean) {
    this.showConfirmModal()
  },
  showConfirmModal() { /* (...) */ }
}
Enter fullscreen mode Exit fullscreen mode

State transition from an action

Action this context contains the $changeState method, which allows you to transition to other states.

INITIAL: {
  handleCheckboxClick(value: boolean) {
    this.$changeState('AGREED')
  }
},
AGREED: { /* (...) */ }
Enter fullscreen mode Exit fullscreen mode

State transition hooks

$onEnter and $onLeave are special actions that are automatically executed when entering or leaving a state. They are essentially hooks that allow you to perform certain actions or side effects during the state transitions.

  • $onEnter: This hook is called when the machine enters a new state. You can use this hook to perform setup actions or to log that a new state has been entered. For example, onEnter() { console.log("Entering STATE_ONE"); } would log "Entering STATE_ONE" whenever the machine transitions into the "STATE_ONE" state.

  • $onLeave: This hook is called when the machine is about to leave a state. You can use this hook to perform cleanup actions or to log that a state is being exited. For example, onLeave() { console.log("Leaving STATE_ONE"); } would log "Leaving STATE_ONE" whenever the machine transitions out of the "STATE_ONE" state¹.

const machine =  createMachine('counter-machine', {
  initial: "INITIAL",
  state: {
    INITIAL: {
      $onEnter() {
        console.log('Entering state "INITIAL"')
      },
      $onLeave() {
        console.log('Leaving state "INITIAL"')
      }
    },
    AGREED: { /* (...) */ }
  }
})

machine.changeState('AGREED')
Enter fullscreen mode Exit fullscreen mode

console output:

Entering state "INITIAL"
Leaving state "INITIAL"
Enter fullscreen mode Exit fullscreen mode

Reactive object

It’s an object that holds the mutable state of your application and is made reactive by Vue.js. This means that Vue.js can automatically track changes to this state and update the DOM when the state changes. In order to use it, add a new "reactive" key to your machine structure.

const machine =  createMachine('counter-machine', {
  initial: "INITIAL",
  reactive: {
    checkbox_checked: false,
    times_clicked: 0
  },
  state: {
    INITIAL: {
      handleCheckboxClick(value: boolean) { /* (...) */}
      showConfirmModal() { /* (...) */ }
    },
    AGREED: { /* (...) */ }
  }
}) 
Enter fullscreen mode Exit fullscreen mode

You can read and mutate reactive object directly from the machine:

machine.reactive.times_clicked += 1
Enter fullscreen mode Exit fullscreen mode

or using this context of some action:

INITIAL: {
  handleCheckboxClick(value: boolean) {
    this.$reactive.checkbox_checked = value
  },
  showConfirmModal() { /* (...) */ }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Returning reactive state from an action

Due to typescript limitations, if you want to return reactive state from an action, you have to specifically set the return type of it. Example:

const machine = createMachine('some-machine', {
  initial: "INITIAL",
  reactive: {
    counter: 1
  },
  states: {
    increment(num: number): number {
      this.$reactive.counter += number
      return this.$reactive.counter
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Resetting reactive to its initial state

To reset reactive to its initial state, you can either call this.$resetReactive() in some action or call machine.resetReactive() directly on the machine.

Preserving the state with the LocalStorage API

The useLocalStorage option in xMachineVue allows you to persist the state of your machine in the browser's local storage. This can be particularly useful if you want to maintain state across browser sessions or refreshes.

When you initialize your machine, you can set useLocalStorage: true to enable this feature. Here's an example:

const machine = createMachine('some-machine', {
  useLocalStorage: true,
  /* (...) */
});
Enter fullscreen mode Exit fullscreen mode

When useLocalStorage is set to true, the state of the machine and any reactive objects are stored in local storage. The key used for storing this data is __XMACHINE__.ID(<machine id>), where <machine id> is the ID of your machine (in this case, 'some-machine').

This means that even if the user closes the browser or refreshes the page, the state of the machine will be preserved and can be reloaded the next time the page is opened. This can be very useful for maintaining user progress or settings across sessions.

Thanks for reading. If you want to support me, you can leave a GitHub star in the xMachineVue repo. :D

links:

Top comments (0)