Search code examples
componentsnuxt3.jsxstatecomposable

Use XState in a nuxt composable


I want to use an xstate state machine in Nuxt 3 which is used over multiple components. I created a small example of how I want this to look like.

I also use the nuxt-xstate module.

State Machine:

export default createMachine(
    {
        id: 'toggle',
        initial: 'switched_off',
        states: {
            switched_on: {
                on: {
                    SWITCH: {
                        target: 'switched_off'
                    }
                }
            },
            switched_off: {
                on: {
                    SWITCH: {
                        target: 'switched_on'
                    },
                }
                
            },
        },

    }

)

Composable:

const toggle = useMachine(toggleMachine)

export function useToggleMachine(){
    return { toggle }
}

app.vue:

<template>
  <div>
    State: {{toggle.state.value.value}}
  </div>
  <br />
  <button
    @click="toggle.send('SWITCH')"
  >
      Switch
  </button>
</template>

<script>
    import { useToggleMachine } from '~/composables/toggle_machine'
    export default { 
        setup(){
            const { toggle } = useToggleMachine()


            return { toggle }
        }
    }
</script>

The problem is, that I can have a look at the state of the machine {{state.value.value}} gives me the expected 'turned_off'. But I cannot call the events to transition between states. When clicking on the button, nothing happens.

Here is the console.log for the passed 'toggle' object:

enter image description here

Does anyone know a way how to fix this, or how to use xstate state machines over multiple components. I am aware that props work, but I don't really want to have an hierarchical approach like that.


Solution

  • In Nuxt3, it's very simple:

    in composables/states.ts

    import { createMachine, assign } from 'xstate';
    import { useMachine } from '@xstate/vue';
    
    const toggleMachine = createMachine({
      predictableActionArguments: true,
      id: 'toggle',
      initial: 'inactive',
      context: {
        count: 0,
      },
      states: {
        inactive: {
          on: { TOGGLE: 'active' },
        },
        active: {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          entry: assign({ count: (ctx: any) => ctx.count + 1 }),
          on: { TOGGLE: 'inactive' },
        },
      },
    });
    export const useToggleMachine = () => useMachine(toggleMachine);
    
    

    In pages/counter.vue

    <script setup>
    const { state, send } = useToggleMachine()
    </script>
    
    <template>
      <div>
        <button @click="send('TOGGLE')">
          Click me ({{ state.matches("active") ? "✅" : "❌" }})
        </button>
        <code>
          Toggled
          <strong>{{ state.context.count }}</strong> times
        </code>
      </div>
    </template>