DEV Community

loading...
Cover image for Rewriting Vuex module to Composition API.

Rewriting Vuex module to Composition API.

lukasborawski profile image Lukas Borawski ・5 min read

Hi there. In this post, I'll show you how to rewrite some Vuex module to the Vue Composition API. It might be a great example of how you can replace the old, good Vue state management system with this new powerful tool from the Vue 3 release.

The module comes from a simple notebook app that I've made for some workshops that I ran a while ago. You can find it here.

So what this module does? In short, it aggregates, saves, and removes notes. Let's look at it briefly.

import { Module, ActionTree, GetterTree, MutationTree } from 'vuex'
import { MainState, NotesState, Note } from '~/types/store'

export const state: NotesState = {
  notes: [],
}

const mutations: MutationTree<NotesState> = {
  setNotes(state, payload: Note[]) {
    state.notes = payload
  },
}
const actions: ActionTree<NotesState, MainState> = {
  async saveNote({ state, commit }, $payload: Note) {
    commit('setNotes', [...state.notes, $payload] as Note[])
    // saving note into the storage
  },
  async removeNote({ state, commit }, $id: string) {
    commit('setNotes', notes.filter() as Note[])
    // removing notes from the storage
  },
  async getNotes({ state, commit }) {
    // getting notes from the storage
    notes.map((note: Note) => {
      commit('setNotes', [...state.notes, note] as Note[])
    })
  },
}
const getters: GetterTree<NotesState, MainState> = {
  notes: ({ notes }) => notes,
}

const namespaced: boolean = true

export const note: Module<NotesState, MainState> = {
  namespaced,
  state,
  mutations,
  actions,
  getters,
}
Enter fullscreen mode Exit fullscreen mode

OK, for some context, we have here Typescript and some types that you can find below. In the app, there is also a $localForage Nuxt module that stores data locally. Check it here. For purpose of this article storing data logic will be removed.

export interface Note {
  id: string
  title: string
  note: string
  date: Date
}
export interface MainState {
  version: string
  $localForage: any
}
export interface NotesState {
  notes: Note[] | {}[]
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s move across this module. From the top, we have - of course - a state with our notes array. Mutations keep notes saving into the state functionality. Then we have actions that add, remove, and read notes from/to storage. One getter to receive current notes at the end.

OK, time to get our hands dirty.

The major and one of the most important things that Composition API allows is to split and move our common business logic into separated blocks (files) called composables. Then reuse them in the whole app.

So we can create one of them now. Place it into the new folder ~/composables as a useNotes.ts file - we're operating with Nuxt structure. Firstly copy the types that will be used the same way as it was with Vuex module.

In the beginning, we have to recreate the state. To do that we will use a new utility that Composition API provides called reactive.


reactive is the equivalent of the current Vue.observable() API in 2.x, renamed to avoid confusion with RxJS observables. Here, the returned state is a reactive object that all Vue users should be familiar with. The essential use case for reactive state in Vue is that we can use it during render. Thanks to dependency tracking, the view automatically updates when reactive state changes.


Tip: check the ref object as well here.

The code:

import {
  reactive,
  computed,
  useContext,
  ComputedRef,
} from '@nuxtjs/composition-api'
import { NotesState, Note } from '~/types/store'

const state: NoteState = reactive({
  notes: [],
})
Enter fullscreen mode Exit fullscreen mode

One thing worth noticing is that we need to define our reactive state object outside the main composable function. We want full reactivity and access to this data from other components. We don't need to export it though.

Time for our composable useNotes.

In the same file we'll define this code:

export default function useNotes(): {
  notes: ComputedRef<Note[]>
  getNotes: () => void
  saveNote: ($payload: Note) => void
  removeNote: (id: string) => void
} {
  const setNotes = ($notes: Note[]) => {
    return (state.notes = $notes)
  }
  const saveNote = async ($payload: Note) => {
    commit('setNotes', [...state.notes, $payload] as Note[])
    // saving note into the storage
  }
  const removeNote = async ($id: string) => {
    commit('setNotes', notes.filter() as Note[])
    // removing notes from the storage
  }
  const getNotes = async () => {
    // getting notes from the storage
    notes.map((note: Note) => {
      commit('setNotes', [...state.notes, note] as Note[])
    })
  }

  return {
    notes: computed(() => state.notes),
    getNotes,
    saveNote,
    removeNote,
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's dive into it. What we have here is a simple function that returns notes from previously defined state and handlers/actions to save, remove, and get notes. Actually, they look exactly the same as the Vuex module ones. Notes are the computed value now which is delivered from Composition API and it's an equivalent of well known computed from Vue Options API.

Done. We got rid of all the Vuex module complexity - no mutations, no actions, no getters. All we need is one functional composable that can be reused anywhere we want in the app.

Additionally, we've provided some typings for returns. And as for notes handling functions is pretty straightforward, that for notes we're using now the generic type for ComputedRef. As of version 3 of Vue we get all the typings out of the box - awesome.

Now we can use it with the real component. In our case, it will be an index page. Data from the useNotes composable will be passed, propagated to the child components as a prop - more about chaining data through props and Composition API soon, stay tuned.

The index.vue page code:

<template>
  <app-notes-list :notes-prop="notes" />
</template>
Enter fullscreen mode Exit fullscreen mode
import useNotes from '~/composables/useNote.ts'

export default Vue.extend({
  name: 'PageIndex',
  setup() {
    const { notes, getNotes } = useNotes()
    onBeforeMount(() => {
      getNotes()
    })
    return {
      notes,
    }
  },
  components: {
    AppNotesList,
  },
})
Enter fullscreen mode Exit fullscreen mode

With Vue 3 we get this new optional syntax with setup function. It allows us to combine all component logic in one place, ordered by logical blocks. The perfect scenario is that you keep your whole business code outside the component and just invoke it along with setup function. And as with our index page example, we've imported the useNotes composable chunks to collect notes.

One new thing that you might remark here is this new function onBeforeMount. And of course, it's a hook. With Composition API there are newly redefined hooks that we can use with setup function.

And that's it. Controversial? A bit? Well, now with Composition API we could get rid of almost all Vuex complexity. From a technical perspective, it will be almost the same, but the way of defining it and acting with will be less complicated. It's just functions that we all know. We don't need mutations, actions, and getters. More we don't have to map them at all. Now just a simple import is enough and we're moving on. And the biggest advantage of the Vuex modules - separation of logic - we can still have with Composition API. One more thing might be speed and performance, but this one needs some benchmarks to confirm. Give it a try, you'll be thrilled.

The whole code is available on this repo with mentioned before a simple notebook app.

Thanks, enjoy.

Discussion

pic
Editor guide
Collapse
jankaderabek profile image
Jan Kaderabek

Hi, nice article, but the github repository is not available, can I check that please?

Collapse
lukasborawski profile image
Lukas Borawski Author

Hi, repo is up again. Sorry about that.