Vue 3 State Management

Hello ๐Ÿ‘‹ ! Today we will talk about state management.

Because my article about the problems I encountered switching from Vue 2 to Vue 3 was quite popular, I decided to continue writing about my journey to switch to Vue 3 and tell you about my experience in choosing a State Management library in Vue 3.

With the new Vue 3 composition API, the options you have for state management are expanding a lot and some of the libraries I will mention here are pretty cool ๐Ÿ’…, you will see.

But, before we continue ๐Ÿ™Œ keep in mind this is my personal preference and I am not saying one is better than the other. I will just show you the top 4 libraries I considered for my project when I switched to Vue 3. Plus I wanted to list them here so I can easily ๐Ÿ” find them in the future.

Vue 3 State Management libraries:

  1. Pinia
  2. Your own
  3. Harlem
  4. Vuex

Let’s take each one and talk a little bit about it and of course see some code ๐Ÿคฉ

1. Pinia

My favorite one and the one I plan to give a try in the future. Currently, the project is experimental, and the goal of the project if you read the description on GitHub is to show what a Store could be like using the composition API.

Let’s see a store example before we can talk more about pros and cons of this library:

// store/projects.js
import { defineStore } from 'pinia'

const url = 'some API URL here'

export const useProjects = defineStore({
  id: 'projects',
  state: () => ({
    projects: [],
    loading: false
  }),
  getters: {
    doneProjects() {
      return this.projects.map(p => p.isDone)
    }
  },
  actions: {
    async fetchAll() {
      try {
        this.loading = true
        this.projects = await (await fetch(url)).json()
        this.loading = false
      } catch (err) {
        // handle error here
      }
    }
  }
})
<template>
  <div v-if="projects.loading">
    <div v-for="project in projects.doneProjects" :key="project.id">
      <div>{{ project.name }}</div>
    </div>
  </div>
  <div v-else>loading</div>
</template>

<script>
import { onMounted } from 'vue'
import  { useProjects } from '@/store/projects'

export default {
  setup() {
    const projects = useProjects()

    onMounted(() => {
      projects.fetchAll()
    })

    return {
      projects
    }
  }
}
</script>

๐Ÿ‘ What I like:

  • no actions, just methods
  • very simple to use and the code looks clean
  • no more mutations, this is the big one for me ๐Ÿฅณ
  • works great with Typescript and has Autocomplete
  • DevTools support
  • SSR support โš™๏ธ

๐Ÿ‘Ž What I don’t like:

  • you don’t have time travel
  • the project is still experimental
  • you can mutate the state by directly change something

๐Ÿค” Conclusion:

Pinia has some great ideas ๐Ÿ’ก and personally, I like all of them. It is clean and straightforward to use plus composing stores and using the $patch method to update state is something that I really like.

Here is the link to the repository give it a try and leave a comment with what you think about it.

2. Your own state management

This is what I currently use. With the addition of Composition API to Vue 3, you can easily build your own state management.

Here is an example:

// store/projects.js

import { reactive, toRefs } from "vue"
const url = 'some API URL here'

const state = reactive({
    projects: [],
    loading: true
});

export default function useProjects() {
    const fetchAll = async () => {
        state.loading = true
        state.projects = await (await fetch(url)).json()
        state.loading = false
    }
    
    const doneProjects = compute(
        () => state.projects.map(p => p.isDone)
    )

    return {
        ...toRefs(state),
        doneProjects,
        fetchAll
    }
}

And then you can use the hook useProjects in any .vue file like this:

<template>
    <div v-if="loading">
      <div v-for="project in projects" :key="project.id">
        <div>{{ project.name }}</div>
      </div>
    </div>
    <div v-else>loading</div>
</template>
<script>
    import { onMounted } from 'vue'
    import useProjects from '@/store/projects'

    export default {
        setup() {
            const {
                loading,
                projects,
                fetchAll
            } = useProjects()

            onMounted(() => {
                 fetchAll()  
            })
            
            return {
                loading,
                projects
            }
        }
    }
</script>

๐Ÿ‘ What I like:

  • very simple to implement and use
  • very simple to test
  • code is reusable, you can copy-paste it
  • works great with Typescript

๐Ÿ‘Ž What I don’t like:

  • you don’t have time travel
  • the state is not immutable, you can edit parts of the state
  • no devtools

๐Ÿค” Conclusion:

This solution is a good choice when your project is not very big and the team is relatively small. Another instance when you can choose this solution is when you want to prototype something or you just need to build an MVP

The best thing here is that you can easily upgrade to another solution if you decide so.

3. Harlem

Harlem is a simple, unopinionated, lightweight, and extensible state management for Vue 3. It’s selling point is that it will not impose any standards or conventions on your codebase (which I don’t agree with) and is very lightweight (around 1KB)

Let’s try to write our example using Harlem.

import { createStore } from '@harlem/core'
const url = 'some API URL here'
const STATE = {
  projects: [],
  loading: false
}

const { getter, mutation, ...store } = createStore('projects', STATE)

export const state = store.state

// getters
export const doneProjects = getter('doneProjects', state => state.projects.map(p => p.isDone))

// Mutations
export const setLoading = mutation('setProjects', (state, payload) => {
  state.loading = payload
})

export const setProjects = mutation('setProjects', (state, payload) => {
  state.projects = payload
})

// actions
export async function fetchAll() {
  try {
    setLoading(true)
    const projects = await (await fetch(url)).json()

    setProjects(projects)
    setLoading(false)
  } catch (err) {
    // handle error here
  }
}

And then here is the .vue file:

<template>
  <div v-if="loading">
    <div v-for="project in doneProjects" :key="project.id">
      <div>{{ project.name }}</div>
    </div>
  </div>
  <div v-else>loading</div>
</template>

<script>
import { onMounted, computed } from 'vue'
import useProjects from '@/store/projects'
import { state, doneProjects, fetchAll } from '@/stores/projects'

export default {
  setup() {
    const loading = computed(() => state.loading)

    onMounted(() => {
      fetchAll()
    })

    return {
      loading,
      doneProjects
    }
  }
}
</script>

๐Ÿ‘ What I like:

  • immutable state
  • it is extensible using plugins
  • DevTools support
  • SSR support โš™๏ธ

๐Ÿ‘Ž What I don’t like:

  • I don’t like the idea of actions & mutations ๐Ÿ˜ข
  • the code is verbose as you can see in the example it take a lot more lines of code for the same example
  • not easy to change to other options

๐Ÿค” Conclusion:

This looks like a nice project but from my point of view, it doesn’t add too much compared to Vuex. Plus if you check the first two examples you can see it is very easy to switch from one option to another but if you go with this choice I see lots of changes to do if you decide to switch.

But if mutations ๐Ÿค are something that you like and you really need time travel then, here is the link to the repository give it a try and leave a comment with what you think about it.

4. Vuex

Vuex is still a solid choice as a state management library. With the release of version 4.0, it is very close to previous examples.

Let’s write the above example using Vuex 4.0

import { createStore } from 'vuex'

export const store = createStore({
  state: {
    projects: [],
    loading: false
  },
  mutations: {
    setProjects(state, payload) {
      state.projects = payload
    },
    setLoading(state, payload) {
      state.loading = payload
    }
  },
  getters: {
    doneProjects(state) {
      return state => state.projects.map(p => p.isDone)
    }
  },
  actions: {
    async fetchAll({ commit }) {
      try {
        commit('setLoading', true)
        const projects = await (await fetch(url)).json()

        commit('setProjects', projects)
        commit('setLoading', false)
      } catch (err) {
        // handle error here
      }
    }
  }
})

And then in any .vue file to access the store within the setup hook, you can call the useStore function

<template>
  <div v-if="loading">
    <div v-for="project in doneProjects" :key="project.id">
      <div>{{ project.name }}</div>
    </div>
  </div>
  <div v-else>loading</div>
</template>

<script>
import { onMounted, computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    onMounted(() => {
      store.dispatch('fetchAll')
    })

    return {
      loading: computed(() => store.state.loading),
      doneProjects: computed(() => store.getters.doneProjects)
    }
  }
}

๐Ÿ‘ What I like:

  • the new useStore hook
  • the removal of the global typings for this.$store
  • DevTools support
  • SSR support โš™๏ธ
  • time travel
  • immutable state

๐Ÿ‘Ž What I don’t like:

  • I don’t like the mutations ๐Ÿ˜ข a lot of extra code to write
  • the code is verbose as you can see in the example
  • not easy to change to other options
  • I don’t like to write again computed props to export my getters
  • I don’t like how you compose stores (modules)

๐Ÿค” Conclusion:

The new Vuex 4.0 made some good improvements but still, you need to write a lot of code for simple operations.

Plus getting the entire store and then export some parts of it using computed props is a lot of extra work I need to do and I am too lazy to do that ๐Ÿ˜œ

You can find the repository here

Final words

As you can see there are lots of options to choose from. Depending on what are your preference and your constraints you can choose the proper library to manage your state.

I wanted to mention V-Bucket too, but I found it to be the same as Vuex and could not found any unique feature that I could show.

Thank you so much for reading!

If there is anything I can do to help, please reach out.ย  Otherwise, if you want to get notified when I post something new please subscribe or follow me on Twitter @ghalex

Have a nice day!

Please wait...

Thank you for subscribing!

8 Comments

Radu December 8, 2020 Reply

Really good list. I’ve been doing #2 and some vuex for a project I migrated but I haven’t made up my mind yet long term.
I don’t think you NEED to use mutations in Vuex. It is recommended but sometimes when I’m in a hurry I mutate directly from the actions and it works just fine.

hipertracker December 17, 2020 Reply

But without mutation Vuex Developers tool is blind because it only traces mutations and ignores actions or direct state changes. It’s very poor comparing to Overmind Developers Tool where you can see everything.

Murray Bauer December 9, 2020 Reply

Nice article. I agree btw.
I would like to see mobx added to list and analyzed.

Alexandru Ghiura December 10, 2020 Reply

Hi Murray,

Thanks for the suggestion, I actually used MobX a lot when working with React. I will make some research and see how it works with Vue and added to the article.

hipertracker December 17, 2020 Reply

You should try Overmind (https://overmindjs.org/). It is simple, scalable, and powerful, and has awesome Developer Tools.

Alexandru Ghiura December 28, 2020 Reply

I will definitely give Overmind a try.

Thanks

Levcsรกk Sรกndor January 17, 2021 Reply

Hi, Alexandru

Thanks for writing such a structured article)

I would like to suggest an amendment about one of Harlem’s conclusions:
> I donโ€™t like the idea of actions & mutations

In your example, async function fetchAll is not actually an ACTION provided by Harlem, it rather looks like your own defined function

In the description of the repositioty there is a specially highlighted Q&A, about actions (https://github.com/andrewcourtice/harlem#what-about-actions), which tell us that “Harlem doesn’t provide a mechanism for actions”

Please update the article, if you agree

Thanks again)

Alexandru Ghiura January 24, 2021 Reply

Hi Sandor,

Thanks for the information, you are right the ‘fetchAll’ function is not an action provided by Harlem.

Still, I do have to write an “async function” to get data and a mutation to change the state which makes the code more verbose as you can see in the example.

I will update the article to make sure people understand the action function is not provided by Harlem.

Thanks,
Alexandru

Leave a Reply