loading...
Cover image for Todo App with Xstate and Vue composition API

Todo App with Xstate and Vue composition API

jasmin profile image Jasmin Virdi ・3 min read

In the last post, I have briefly discussed the use of State Machines and their advantages in Frontend Applications. Xstate library has made it easier in leveraging State Machines in our Frontend Applications.

I wanted to try the Xstate library so I thought of recreating my simple Todo App which was built using Vue 2. This exercise has helped me in learning about State Machines use in real applications and new features of Vue 3 which is the composition API 🤩.

Project Walkthrough

I used the package @vue/composition-api as I was planning to recreate an existing Todo Application.

The Todo application is primarily based on the Actor Model where a new machine actor is created by reference with a unique identity and the parent machine is automatically subscribed to the spawned child machine's state.

fetchList: assign({
    todoList: (context, event) => {
        return context.todoList.map((todo) => ({
            ...todo,
            ref: spawn(todoItemMachine.withContext(todo))
        }))
    }
})

This action on the state change of todoMachine will spawn a new actor of todoItemMachine which will be scoped to the service where it is spawned.

The classification of machines was based on the idea that actions related to TodoList like fetch, edit, delete and create will be in one place and the action to mark the todo as completed will be in one place 😉

The todoItemMachine is defined with only two actions that are completed and pending.

const todoItemMachine = Machine({
    id: 'todoItem',
    initial: 'completed',
    states: {
        completed: {
            on: {
                completeTask: {
                    actions: 'completedTodoAction'
                }
            }
        },
        pending: {}
    },
    actions: {
        completedTodoAction: assign((context, event) => {})
    }
})

With this, our state logic is ready to be integrated so let's start with integration 👩🏻‍💻

I have defined a separate function to handle all the state machine actions in one place so that they can be easily shared across multiple components. Vue composition API has made it possible to keep all the logic related to one entity in one place in the form of functions instead of being separated by optional properties like the earlier versions of Vue.

import { todoMachine } from './index'
import { useMachine } from '@xstate/vue'
import { reactive, computed } from '@vue/composition-api'
import { store } from '../store/todoActions'

export default function stateMachineActions() {
    const {
        state,
        send
    } = useMachine(todoMachine)

    let todoActionStore = reactive({
        store
    })

    function setCurrentState(state) {
        store.commit('setState', state)
    }

    function stateTransitions(action, payload) {
        send({
            type: action,
            payload
        })
    }

    return {
        state,
        stateTransitions,
        todoActionStore,
        setCurrentState,
        todoList: computed(() => state.value.context.todoList)
    }
}

The current state of the machine is saved in the store using the setCurrentState function.

The stateMachineActions function is included in the components and used whenever we want to dispatch the event and transition state from present to next.

import stateMachineActions from './xstate-todo/generateTodoStateMachine'
export default {
    name: 'app',
    /*
     */
    setup(props, context) {
        let {
            state,
            stateTransitions,
            todoActionStore,
            setCurrentState,
            todoList
        } = stateMachineActions()


        function completeTodoItem(todoItem) {
            setCurrentState('editTodoItem')
            stateTransitions('editItem', todoItem)
        }

        return {
            state,
            createTodo,
            todoActionStore,
            todoList,
            deleteTodoItem,
            completeTodoItem
        }
    }
}

In order to observe any changes in the spawned actor due to state changes in the parent, machines listen to useService hook. In this case, if any change occurs in the todo item we can observe the todoItemMachine actor and update the UI accordingly.

setup(props, context) {
        let {
            state,
            send
        } = useService(props.todo.ref)

        /*
         */
        return {
            todoItem: computed(() => state.value.context),
            isEditing,
            completeTodo,
            deleteTodo,
        }

ActorRef is placed within the context of the parent Machine.

You find the complete code on Github and CodeSandbox

Conclusion

This is was great fun and learning exercise. I really enjoyed exploring the library and the new Vue composition API. The official documentation along the examples helped me a lot to complete this exercise. You can refer the following documentation for more details 📝

Posted on by:

Discussion

markdown guide