Home / Blog / How to Migrate from Vuex to Pinia
How to Migrate from Vuex to Pinia

How to Migrate from Vuex to Pinia

Daniel Kelly
Daniel Kelly
June 28th 2022

Now that Pinia is the officially recommended global state management solution for Vue.js, are you eager to migrate your global store from Vuex? If so, you’ve come to the right place! Let me show you how I did just that, using the code-base for our Vue.js 3 Master Class.

Why Migrate from Vuex to Pinia?

Before we dive into the how-to, let’s quickly discuss why you might consider doing this. Some advantages of Pinia over Vuex include:

  • Pinia is modular by default (which encourages good organization by logical concern and supports code splitting)
  • Pinia removes mutations 🎉 (no more writing a brand new mutation to update a single piece of state)
  • Pinia is more intuitive (feels like regular JavaScript - reading properties and calling methods, and less concepts to learn than Vuex)
  • No “magic strings” to keep up with (mutation and action names)
  • No context object to fuss with in action params
  • Pinia has full type support for TypeScript

Personally, I think the DX improvement in Pinia justifies the switch but weigh the pros and cons for yourself. Depending on the size of your app and the preferences of your team, it might not make sense to migrate right away. As far as I know, support for Vuex isn’t going anywhere anytime soon. But, if you’re convinced it’s time to migrate to Pinia then this article is for you.

Setting the Context

To help you best follow this tutorial, first there are a couple things you should know about the code-base we’re working on.

  1. It uses Vue CLI
  2. It’s on Vue version 3
  3. It uses plain JavaScript (no TypeScript)

With this in mind, a couple of these steps might look a little different for you depending on your setup, but it should be mostly the same.

Install Pinia and Modify Webpack Config

The first step in migrating is installing the pinia npm package.

npm install pinia

I chose to leave Vuex installed during the migration and then only remove it once the migration is complete. This strategy means the app will continue to run and I can incrementally migrate one store at a time.

Next, you’ll want to install the Pinia plugin in main.js.

import { createPinia } from 'pinia'
app.use(createPinia())

After this, I started up the dev server and got a bunch of compile errors that looked like this.

webpack compile errors

This is specific to WebPack and can be fixed with the following config added to vue.config.js.

module.exports = {
  configureWebpack: {
    module: {
      rules: [
        {
          test: /\.mjs$/,
          include: /node_modules/,
          type: 'javascript/auto'
        }
      ]
    }
  }
}

You can see this issue in github for more info if you’re interested.

Define Stores from Vuex Modules

Next, I chose my Vuex module called auth.js to convert to a Pinia store. It was by no means the shortest of my stores but I think it makes for the best example for the tutorial. Here is a stripped down version of it:

// src/store/modules/auth.js (Vuex)
export default {
  namespaced: true,
  state: {
    authId: null,
    authUserUnsubscribe: null,
    authObserverUnsubscribe: null
  },
  getters: {
    authUser: (state, getters, rootState, rootGetters) => {
      return rootGetters['users/user'](state.authId)
    }
  },
  actions: {
    signInWithEmailAndPassword (context, { email, password }) {
      return firebase.auth().signInWithEmailAndPassword(email, password)
    },
        async signOut ({ commit }) {
      await firebase.auth().signOut()
      commit('setAuthId', null)
    },
        //9 more actions existed here...
  },
  mutations: {
    setAuthId (state, id) {
      state.authId = id
    },
    //2 more mutations existed here 
        // (each for updating a single piece of the state above 🤮)... 
  }
}

We can create something similar to this Vuex “module” in Pinia by defining what’s known as a Pinia “store”. It’s common convention to create Pinia stores in a directory called stores and I like to name them with the following formatting: [StoreName]Store.js. So that’s what I’m doing here with the boilerplate code for a Pinia store.

// src/stores/AuthStore.js (Pinia)
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('AuthStore', {
  state: () => {
    return {

    }
  },
  getters: {},
  actions: {}
})

Convert State Defined in Vuex to Pinia

Now we can start moving things from the Vuex auth module to the Pinia AuthStore. The state itself is pretty much a one-to-one port. I can just copy and paste it.

// src/stores/AuthStore.js (Pinia)
export const useAuthStore = defineStore('AuthStore', {
  state: () => {
    return {
      authId: null,
      authUserUnsubscribe: null,
      authObserverUnsubscribe: null
    }
  },
    //...
})

Of course, accessing the state is a different matter entirely, but we’ll get to that after we’re done converting the Vuex module definition to a Pinia store definition.

Convert Getters Defined in Vuex to Pinia

Next, I’ve got a single getter inside auth.js that reaches into a different Vuex module to get the actual user based on the authId in the state.

// src/store/modules/auth.js (Vuex)
getters:{
    authUser: (state, getters, rootState, rootGetters) => {
   return rootGetters['users/user'](state.authId)
  }
}
//...

In order to focus on a single module at a time, I’ll continue to use the users Vuex module by importing the Vuex store and accessing the getters on that, since rootGetters aren’t available in Pinia getters.

// src/stores/AuthStore.js (Pinia)

import store from '@/store' // this is the Vuex store (and is temporary until migration is complete)
//...
getters:{
    authUser(){ // should be a non-arrow function now
      return store.getters['users/user'](this.authId) // get authId on `this`
    }
}

Notice that I no longer have to supply all the different arguments to my getter. I can also access the authId piece of state directly on this instead of on a passed state parameter.

I did choose to change the function declaration to the non-arrow style so that this would properly refer to the store. Had I kept the arrow function, I could have accessed the state like this:

authUser: (state) =>{ 
 return store.getters['users/user'](state.authId) // get authId on `state`
}

Convert Actions Defined in Vuex to Pinia

Next up, we need to take care of the actions. This is what they look like in Vuex along with the mutations they call.

// src/store/modules/auth.js (Vuex)
export default {

  actions: {
    signInWithEmailAndPassword (context, { email, password }) {
      return firebase.auth().signInWithEmailAndPassword(email, password)
    },
        async signOut ({ commit }) {
      await firebase.auth().signOut()
      commit('setAuthId', null)
    },
        //9 more actions existed here...
  },
  mutations: {
    setAuthId (state, id) {
      state.authId = id
    },
        // ...
  }
}

The first is straight forward since it doesn’t alter any state. I just need to import firebase and remove the context argument since Pinia actions don’t need it.

// src/stores/AuthStore.js (Pinia)
import firebase from '@/helpers/firebase'

actions: {
  signInWithEmailAndPassword ({ email, password }) {
    return firebase.auth().signInWithEmailAndPassword(email, password)
  }
}
//...

The signOut action does call a mutation to update the state.

// src/store/modules/auth.js (Vuex)
async signOut ({ commit }) {
  await firebase.auth().signOut()
  commit('setAuthId', null)
},

In Pinia, we no longer need to call mutations. Instead we can alter the state directly.

// src/stores/AuthStore.js (Pinia)
async signOut ({ commit }) {
  await firebase.auth().signOut()
  this.authId = null
}

This means we have no need to port any of the mutations over 💪. Furthermore, we no longer need the commit function from the context object. This concept simply doesn’t exist in Pinia.

// src/stores/AuthStore.js (Pinia)
async signOut () { //..

Final Steps for Moving auth Vuex Module to Pinia AuthStore

At this point, I migrated the other 9 actions that existed in my auth.js Vuex module and the process looked pretty much like those steps above except for:

  • I also modified lines where dispatch was used to call other actions defined in the same module to reference actions on this

    // this in Vuex
    *await* dispatch('fetchAuthUser')
    
    // became this in Pinia
    *await* this.fetchAuthUser()
  • For actions called from other stores I dispatched them temporarily from the imported Vuex store. (I did likewise for mutations defined in the root level store but commited in auth.js)

    import store from "@/store"
    
    // this in Vuex
    dispatch('users/createUser')
    // became this in Pinia (this is just temporary!)
    store.dispatch('users/createUser')
    
    // likewise this in Vuex
    commit('setItem', { resource: 'posts', item }, { root: true })
    // became this in Pinia (this is just temporary!)
    store.commit('setItem', { resource: 'posts', item }, { root: true })
  • I also noted down that AuthStore uses the Vuex users module and a mutation from the root Vuex store. This was so that I could come back to it once the users Vuex module had been migrated. (Don't judge my handwriting 😆)

    sticky note of modules

  • In the rare case I defined a Vuex action with an arrow function, I converted that to a non-arrow function for Pinia so that this would properly reference the Pinia store.

    // this in Vuex
    fetchAuthUser: async ({ dispatch, state, commit }) => {/*...*/}
    
    // became this in Pinia
    async fetchAuthUser () {/*...*/}
  • I also took special care to remove the context object as the first argument of the actions.

My remaining mutations, I left behind all together as they are not needed in Pinia 🥳.

With that, my auth.js Vuex module was no longer needed, so I removed it’s import and registration in the root Vuex store.

// src/store/index.js
// ...
~~import auth from './modules/auth'~~ //removed this
export default createStore({
  modules: {
    //...
    ~~auth~~ // and removed this
  },
  //...
})

(Just before this I also cmd clicked on the export default in auth.jsto be sure it wasn’t being used anywhere else.)

screenshot of VS code after cmd click to check where all auth module was being imported

Finally I had the please of deleting that old Vuex module 🔥!

screenshot of deleting auth Vuex module in VS code

Update Component-Side Interactions

I must admit that first bit felt really nice! But we still have some work to do on the auth Vuex module… we must now update it’s action calls and state retrievals within our components. To figure out where all these were being called I search for 'auth/ throughout the project, which gave me 11 results. Most were in components but 2 were in the file where I setup and register my routes with Vue Router.

screenshot of search results for quote auth slash in VS code

Call Pinia Action Via the Options API

Now I have 2 options with how to proceed. I can replace the Vuex dispatches with calls to the Pinia actions via the options API or the Composition API. Pinia is cool like that 😎. It supports both. Let’s start with the Options API.

In TheNavbar.vue, I’m dispatching the signOut method when the user clicks a Sign Out link.

// TheNavbar.vue
<a
  @click.prevent="$store.dispatch('auth/signOut'),
  $router.push({name: 'Home'})"
>
    Sign Out
</a>

Instead of accessing the store on $store like with Vuex, we should import our Pinia AuthStore composable into the component.

// TheNavbar.vue
import { useAuthStore } from '../stores/AuthStore'

Then we can also import the mapActions helper from Pinia and use it to map our signOut action to a local component method.

// TheNavbar.vue

import { mapActions } from 'pinia'
export default{
  methods:{
    ...mapActions(useAuthStore, ['signOut'])
  }
  //...
}

Lastly, we can update the click handler for the Sign Out link.

// TheNavbar.vue
@click.prevent="signout() //..."

Call Pinia Action Via the Composition API

Next, let’s take a look at using the composition API instead. In SignIn.vue within a signIn method, I’m dispatching the signInWithEmailAndPassword action.

// SignIn.vue

methods:{
  async signIn () {
    try {
      await this.$store.dispatch('auth/signInWithEmailAndPassword', { ...this.form })
      this.successRedirect()
    } catch (error) {
        alert(error.message)
    }
  },
}

Once again I’ll import the useAuthStore composable but this time I’ll call it from within the setup option and destructure the desired action out of it.

setup () {
  const { signInWithEmailAndPassword } = useAuthStore()
},

To expose the action to the rest of the component, I’ll return it from setup.

setup () {
  const { signInWithEmailAndPassword } = useAuthStore()
  return { signInWithEmailAndPassword }
},

Finally, I’ll replace the dispatch with a direct call to the action.

// replace this
await this.$store.dispatch('auth/signInWithEmailAndPassword', { ...this.form })
// with this
await this.signInWithEmailAndPassword({ ...this.form })

Check Progress in the Browser

Following the pattern above, I continued replacing all instances of Vuex dispatches with Pinia action calls. At this point, I was feeling pretty confident, so I went to try my luck in the browser 🤞. Somewhat surprisingly but much to my delight the home page loaded just fine. However, in the console I get 2 store related errors.

screenshot of console errors

Ah… I forgot about mapActions and mapGetters. A search of mapActions('auth reveals 2 changes I need to make…

search results for mapActions('auth in VS code

And mapGetters('auth three more.

search results for mapGetters('auth in VS code

Read Pinia Getter Via the Options API

The instances of mapAction I was able to handle just like the calls to dispatch previously, however reading the getters present a scenrio we haven’t seen yet. Let’s explore these further.

In TheNavbar.vue there’s an instance of mapGetters being used to get the authUser.

// TheNavbar.vue
computed: {
  ...mapGetters('auth', ['authUser'])
},

To make this work with Pinia and the Options API I can import mapState from Pinia and replace the call to Vuex mapGetters with it.

// TheNavbar.vue

import { useAuthStore } from '../stores/AuthStore'

~~import { mapGetters } from 'vuex'~~ // remove this
import { mapActions, mapState } from 'pinia' // add this

computed:{
    ~~...mapGetters('auth', ['authUser']),~~ // remove this
  ...mapState(useAuthStore, ['authUser']) // add this
}

Do take note that in Pinia there is NO mapGetters. When used with the options API, mapState is useful for accessing both state AND getters.

Read Pinia Getter Via the Composition API

In Profile.vue I’ve got another instance of mapGetters('auth.

// (with Vuex)
...mapGetters('auth', { user: 'authUser' }),

This time, let’s switch it out using the composition API. Since there’s already a setup option in Profile.vue that uses an action from the AuthStore you might think I could access the getter like so:

// (with Pinia)
setup () {
  const { fetchAuthUsersPosts, authUser } = useAuthStore() // ❌ won't work
  return { fetchAuthUsersPosts, authUser }
},

But this won’t work. In order to retain reactivity when destructring getters and state from stores, we need to use the storeToRefs helper function.

// (with Pinia)
import { storeToRefs } from 'pinia'
//...
setup () {
  const { fetchAuthUsersPosts } = useAuthStore()
  const { authUser } = storeToRefs(useAuthStore())
  return { fetchAuthUsersPosts, authUser }
},

Finally, if you’ve got a keen eye, you’ll have noticed we were renaming the authUser getter inside the Vuex mapGetter helper to helper.

// (with Vuex)
...mapGetters('auth', { user: 'authUser' }),

We can do this as well for Pinia.

// (with Pinia)
setup () {
  //...
  return { fetchAuthUsersPosts, user: authUser }
},

I also took care of the last mapGetter this way as well.

Update Reads from the State throughout the App

Next, we can take care of all the instances where we’re reading state from the old Vuex module like this: $store.state.auth.

A regular expression search reveals 3 locations that require updates.

search results for store.state.auth

I did the regx here so that I could search for instances with and without the starting $. This was so that I could catch cases outside of components where I might have directly imported the store. To fix these, you can use the same solutions as for the getters.

This is the solution for PostList.vue:

// this in Vuex
<a v-if="post.userId === $store.state.auth.authId" //...

// became this in Pinia
<a v-if="post.userId === authId" //...
//...
import { useAuthStore } from '@/stores/AuthStore'
import { mapState } from 'pinia'
//...
computed: {
  ...mapState(useAuthStore, ['authId']),
},

The solution for the 2 occurences in router/index.js is similar but since we were not in the context of a component where refs are auto-unwrapped, I had to specify .value.

// src/router/index.js
router.beforeEach(async (to, from) => {
  const { authId } = storeToRefs(useAuthStore())
  //...
  if (to.meta.requiresAuth && !authId.value /* <- used .value here */) { 
    //...
  }
})

Update Reads from the State in Vuex Modules

The last step is to change out insances INSIDE remaining Vuex modules where the auth module state is being accessed on rootState.auth.

This step is optional if you plan to do all your migration at one time, however I highly suggest doing it nonetheless. Yes, you will be deleting these Vuex modules soon but it can help you focus on one store at a time, running tests, and doing manual testing after finishing each module to store transition. Plus, when you do copy your actions over to new Pinia stores for modification, this access of state from other stores will already be setup correctly. Thus, I highly recommend only moving on to the next Vuex module once the app is totally working.

A search for rootState.auth reveals 6 results.

search results for rootState.auth in VS code

We can fix them all by changing out rootState.auth.authId to the authId from the Pinia store like this.

// /src/store/modules/posts.js
import { useAuthStore } from '@/stores/AuthStore'
//...
actions:{
  async createPost ({ commit, state, rootState }, post) {
      const authStore = useAuthStore() // use AuthStore 

            // this in Vuex
      post.userId = rootState.auth.authId
      //becomes this in Pinia
            post.userId = authStore.authId
  }
  //...
}

Just make sure you’re calling const authStore = useAuthStore() in each action that you attempt to access authStore.authId in that way it’s actually defined in the current scope. You might be tempted to call useAuthStore() at the top of the file but this won’t work since, you can only use Pinia stores in the context of the Vue application when the root Pinia store is defined.

Conclusion

After spending about 6 hours going through this process, I was able to successfully move the auth Vuex module to a Pinia AuthStore. It had a total of 3 pieces of state, 1 getter, and 11 actions to migrate with access to such actions and state found in 25 places throughout the app. Mind you, that time includes writing this article as well. Thus, you will probably spend less time migrating a single Vuex module to a Pinia store.

I did not finish the migration completely before publishing this article, only the migration of the single auth module. However, my other 5 Vuex modules I suppose will probably just be a matter of “rinse and repeat”.

Once I’ve had the opportunity to finish the process I’ll release a second article detailing any further lessons learned or issues encountered.

In conclusion, the process was not a very difficult one though it does take a little time. The Vue.js 3 Master Class source code is an educational app but it is no small piece of software. It includes all the trappings of a real-world Vue.js application and should be a decent representation of a migration for your application.

So what are you waiting for? Get that migration off the ol’ backlog and into production! 😃

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is a full-time educator at Vue School and has a massive passion for Vue.js, Nuxt.js, and Laravel.

Latest Vue School Articles

Vue.js Forge Episode 1 In Review

Vue.js Forge Episode 1 In Review

The first ever Vue.js Forge was an event to remember! More than 2,500 coders worldwide got hands-on Vue.js experience with the latest technologies.
Daniel Kelly
Daniel Kelly
Infinite Scrolling in Vue with Apollo

Infinite Scrolling in Vue with Apollo

Learn to implement infinite scrolling in Vue with Apollo GraphQL. We’ll cover custom cache behaviour, cursor-based pagination, and using composables to query data.
Michael Thiessen
Michael Thiessen