Migrate from Vuex4 to Pinia in parallel

C.Chambers
Smartbox engineering
10 min readMar 3, 2023

This article is a follow-up article after migrating from Vue Cli to Vite you can read this here. (coming soon)

Packages used at the start of the article:
vite
vuex4
vue3
vue test utils

Introduction:

During the migration of the my-account section of e-commerce websites, we were tasked with migrating this area of the websites to microservices based on vue3 as a standalone UI and a Symfony API for the backend, For the UI we decided to use a Vue Cli setup for our project. We started developing the UI and then later decided to upgrade to Vite and Pinia due to the need to keep the project evergreen, another factor was overall support as Vite and Pinia became the suggested frameworks for the Vue ecosystem.

Why we needed to migrate to Pinia:

  1. Pinia is now the official state management solution for Vue

Although Pinia is the official state management solution for Vue, Vuex will still work with your Vue3 projects and in this article, I will show you how you can migrate from vuex4 to Pinia in parallel allowing you to migrate in smaller more manageable increments.

The current setup on our project:

current Vue Cli / vuex4 setup

As you can see we are using a store module layout which has files for, getters, actions and mutations. We have quite a few files but one of the good things about the migration from Vuex to Pinia is that Pinia removes the need for mutations which means we can ignore all the mutations.ts files.

Let’s get Started — Install Pinia

To import Pinia on your project use the following command in your terminal.

We will leave Vuex installed in our project since the full website currently uses Vuex, Once fully migrated to Pinia we will remove Vuex.

npm install pinia

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

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

The convention is to create a folder named /stores, where you will create all your Pinia store files.

Next, I selected the Vue store which I wanted to migrate, I decided to choose the browser module this module is a very simple store which sets/gets and mutates the local storage in the browser.

I chose this module to show the migration process plus it also calls 2 separate Vuex stores within this store. Auth store and env store.

structure of the vuex browser store
// actions.ts

export const actions = {
setLocalStorageAccessToken: ({ commit, rootGetters }: any, token: string) => {
const storeCode = rootGetters['env/GET_STORE_CODE'].toLowerCase();
localStorage.setItem(`${storeCode}_my_account_app`, token);
commit('auth/UPDATE_STORE_AUTH_TOKEN', token, { root: true });
},

removeLocalStorageAccessToken: ({ commit, rootGetters }: any) => {
const storeCode = rootGetters['env/GET_STORE_CODE'].toLowerCase();
localStorage.removeItem(`${storeCode}_my_account_app`);
commit('auth/UPDATE_STORE_AUTH_TOKEN', null, { root: true });
},
};
// getters.ts

export const getters = {
getLocalStorageAccessToken: (_: any, __: any, rootState: any) => rootState.auth.authToken,
};
// index.ts

import { actions } from './actions';
import { getters } from './getters';

const state = () => {
return {};
};

const browser = {
namespaced: true,
state,
actions,
getters,
};

export default browser;

Creating your first Pinia Store:

Create a new file in your /stores folder for this article it's named browserStore.ts the convention is to name your stores with your name appended with “Store” eg. [StoreName]Store.ts.

Let's look at the code which will be placed inside this file. The following code is the standard template code for a new Pinia Store.

import { defineStore } from 'pinia';   // import pinia

export const useBrowserStore = defineStore('BrowserStore', {
state: () => {
return {};
},
getters: {
},
actions: {
},
});

Moving Vuex state to Pinia state.

This is very simple, lucky for us Vuex state and Pinia state are 1 to 1 copies, so simply copy and paste your state from your Vuex store to your Pinia store and job done! In my case, I have no state so the state method in my Pinia store will return an empty object.

Moving Vuex getters to Pinia

To move your Vuex getters to the Pinia store create your action inside the actions section of your Pinia store.

This example is straightforward since it doesn’t alter any state since I'm using local storage as my permanent state. I need to import our Vuex store and remove the context argument and any other parameters that are associated with Vuex, since Pinia's actions don’t need these parameters

This example will show you how to use another Vuex store inside your Pinia store allowing you to incrementally migrate your project from Vuex to Pinia.

import { defineStore } from 'pinia';

// this is the Vuex store (this temporary until migration is complete)
// since this store depends on our vuex auth store in our project
// TODO remove vuex dependancy
import { store } from '@/store';

export const useBrowserStore = defineStore('BrowserStore', {
state: () => {
return {};
},
getters: {
getLocalStorageAccessToken: () => {
// access the vuex auth store using your imported vuex store
return store.state.auth.authToken;
},
},
actions: {
},
},
});

As you can see in the code snippet above we import the current Vuex store to allow access to the Vuex auth module, We have removed context and any other parameters that are used for Vuex as Pinia does not need them. We have added a TODO comment to allow us to remind ourselves to remove this dependency once we migrate the auth store to Pinia.

We then call the auth store using store.state to access the Vuex stores state.

Moving Vuex actions to Pinia

Migrating Vuex actions to Pinia actions is also pretty straightforward.

Let's have a look at our Vuex getters for the browser module.

//vuex  browser getters module

export const actions = {
setLocalStorageAccessToken: ({ commit, rootGetters }, token) => {
const storeCode = rootGetters['env/GET_STORE_CODE'].toLowerCase();
localStorage.setItem(`${storeCode}_my_account_app`, token);
commit('auth/UPDATE_STORE_AUTH_TOKEN', token, { root: true });
},

removeLocalStorageAccessToken: ({ commit, rootGetters }) => {
const storeCode = rootGetters['env/GET_STORE_CODE'].toLowerCase();
localStorage.removeItem(`${storeCode}_my_account_app`);
commit('auth/UPDATE_STORE_AUTH_TOKEN', null, { root: true });
},
};

We can move this code to our Pinia store file like below, As you can see we remove the commit and root getters parameters from our actions as they are not used in Pinia stores.

//pinia browserStore.ts

import { defineStore } from 'pinia';

// this is the Vuex store (and is temporary until migration is complete)
import { store } from '@/store';

export const useBrowserStore = defineStore('BrowserStore', {
state: () => {
return {};
},
getters: {
getLocalStorageAccessToken: () => {
// access the vuex auth store using your imported vuex store
return store.state.auth.authToken;
},
},
actions: {
setLocalStorageAccessToken: (token) => {
// we use "store" which in our case refrences our vuex store
// we call our vuex stores getters as normal
const storeCode = store.getters['env/GET_STORE_CODE'].toLowerCase();
localStorage.setItem(`${storeCode}_my_account_app`, token);
// we use "store" which in our case refrences our vuex store and its mutate command.
store.commit('auth/UPDATE_STORE_AUTH_TOKEN', token, { root: true });
},
removeLocalStorageAccessToken: () => {
const storeCode = store.getters['env/GET_STORE_CODE'].toLowerCase();
localStorage.removeItem(`${storeCode}_my_account_app`);
store.commit('auth/UPDATE_STORE_AUTH_TOKEN', null, { root: true });
},
},
});

Let's review what we have done so far:

  1. Installed and imported Pinia alongside Vuex
  2. Created our 1st Pinia store named browserStore.ts
  3. Migrated our state, actions and getters from our Vuex store
  4. Allowed our new Pinia store to work alongside existing Vuex stores

Sounds good so what next?

Well, now we have the task of replacing all references of the migrated Vuex store in our Vue components since we want to use our new Pinia store.

The easiest way to do this is to use your IDE and search for your Vuex store getters and actions and replace them like below.

For this project I used my IDE to search for:

// this would find any calls to the getters or actions of the vuex 
'browser/'

In the results we then update the files as follows:
1. Adding the new Pinia store
2. Keeping the references to Vuex in case we still use any other Vuex store in this file.

<script lang="ts" setup>
import { onMounted } from 'vue';
// import your pinia store
import { useBrowserStore } from '@/stores/browserStore';
// keep your vuex store in case you are using other stores in your vue modules
import { useStore } from '@/store';
// use your pinia store
const { removeLocalStorageAccessToken } = useBrowserStore();
// remember to keep your vuex store as usual
const store = useStore();
// find the refrences to your vuex calls
} else {
await store.dispatch('browser/removeLocalStorageAccessToken');
await router.push('/login?token=expired');
}

// and replace them with your new pinia store action
} else {
removeLocalStorageAccessToken();
await router.push('/login?token=expired');
}

Once you have replaced all your references to the old Vuex store you can now delete the old Vuex store that you have migrated from your project.

As you can see we now do not have a browser folder in our Vuex store folder, and we can see we have our new Pinia store under the folder named stores.

You should now check your project and see that the project operates as expected.

Updating your projects Unit tests to use Vuex and Pinia stores

If we run our unit tests our tests should fail as we have removed our Vuex store, this is exactly as we expected.

So the big question is how can I update my unit tests to use a Vuex store and Pinia store in the same tests?

For this article, we are using the following testing packages

  1. Vitest
  2. Vue test utils

Vitest is now the recommended testing package for Vue projects, also using Vue test utils makes writing unit testing Vue apps much nicer.

I have created an article on migrating from jest to Vitest here!

Let's get started updating our Vitest unit tests

This is actually much simpler than you would expect. We need to inject Pinia into our tests, let's start by reviewing our failing tests and add Pinia as a plugin to each of the failing tests.

The majority of our failing tests look like so because we have references to the Pinia store in these files and Vitest does not understand what to do with these Pinia stores:

failing tests due to no Pinia plugin

To resolve these failures we will add Pinia to each of the files that fail.

This can be achieved with createTestingPinia(), which returns a Pinia instance designed to help unit test components.

npm i -D @pinia/testing

Make sure to add Pinia as a plugin for each test in each file.

// import pinia into your tests
import { createTestingPinia } from '@pinia/testing';


describe('example test', () => {
var wrapper;
beforeEach(() => {
wrapper = shallowMount(example, {
global: {
provide: {
[STORE_INJECTION_KEY]: store,
},
// add pinia as a plugin to each of your tests
plugins: [createTestingPinia()],
mocks: {
$t: (phrase) => phrase,
},
},
});
});

Once you do this you should remove the majority of your failing tests, the only tests that should still be failing are the tests you need to mock your responses for.

In my case I have one failing test left, this is due to the need to mock an action to always fail.

failing test due to the need to mock an action in Pinia

How to mock a Pinia getters/actions in Vitest?

So to fix this unit test I need to mock the Pinia action you can see how I resolved this by mocking the Pinia action below.

// Name ofaction to be mocked found in your new pinia store
setLocalStorageAccessToken
// import pinia into your tests
import { createTestingPinia } from '@pinia/testing';
// import your pinia store which you want to mock
import { useBrowserStore } from '@/stores/browserStore';

describe('example test', () => {
var wrapper;
beforeEach(() => {
wrapper = shallowMount(example, {
global: {
provide: {
[STORE_INJECTION_KEY]: store,
},
// add pinia as a plugin to each of your tests
plugins: [createTestingPinia()],
mocks: {
$t: (phrase) => phrase,
},
},
});
});

// mock pinia action using the store you have imported
const browserStore = useBrowserStore();
browserStore.setLocalStorageAccessToken = vi.fn(() => {
throw new Error('localstorage save failed');
});

After doing this you have now defined the mock for your Pinia store action/getter. Run your Vitest suite once again and you should have a sea of green unit tests.

What's next?

The next step is to migrate the next Vuex store, and in my case, it is the auth Vuex store because it's imported into my only Pinia store. If we can migrate the auth store and then the env store to Pinia we can remove the Vuex reference in our browserStore.ts file and rinse and repeat this step for each of the stores in our Vuex store's folder.

Once all your Vuex stores have been migrated to Pinia you can remove the Vuex package from your project.

I hope you found this article useful.

--

--