Recently I had the experience of migrating a Vue web app to a new state management library, Pinia, which was interesting from both a technical and non-technical point of view. In this article I’ll share my thoughts and findings on when, why and how you might consider carrying out such a major technical migration, applicable to any large web project, and some tips and advice on migrating from Vuex to Pinia specifically.
For the purpose of this article I’ll assume you understand something of reactive web applications and state management. It won’t be a step-by-step “how to” tutorial — the official Pinia docs are the best source for that. But some of the things I’ll document here might help you if you’re considering a similar migration.
About the projectpermalink
I work on a large, B2B web application for wind farm operators to visualise energy production, monitor the health of their fleet of turbines, and plan maintenance. The app is a client-side SPA (single-page application) built with Vue, using the Vuex library for state management.
As a Senior Front-End Developer, I am the primary developer on the project, contributing most of the front-end code. I have several years of experience working with Vue and Vuex prior to my current role and know the codebase inside out. I would say this is pretty much a prerequisite for making such a major architectural decision!
About Piniapermalink
Pinia is now well-established as the official state management library for Vue. Vuex continues to be maintained, but there are no plans to add new features. The Vue and Vuex documentation sites recommend using Pinia for new projects.
Compared with Vuex, Pinia is built to be compatible with Vue’s Composition API. If you’re unfamiliar with the Composition API I would recommend getting comfortable with that first, before considering a migration to Pinia.
Pinia can be used in a project alongside Vuex, so you don’t necessarily need to migrate an entire project all at once. Having a store that’s broken up into smaller modules will make it far easier to maintain both frameworks in parallel.
Why migrate?permalink
When considering a migration, the first (and obvious) question to ask is, “Is this change needed at all”? Or alternatively, “What would happen if I left things as they are?” In the case of our project, it’s likely I could continue to use Vuex for some time with no adverse effects.
On the downside, it’s possible that other, external libraries might no longer maintain compatibility, Vuex eventually gets deprecated and stops receiving security updates, or that reliable sources of tutorials become harder to come by, making it harder to onboard new developers. Adding more and more features while continuing to use Vuex will also make it more difficult and time-consuming to migrate in the future as the codebase grows.
It’s worth stepping back and thinking objectively about the real-world effects of a decision to migrate to a new technology. Taking such a big decision simply because something is the “new hotness” is probably not a good idea.
Key considerationspermalink
With any major planned technical change, we need to consider how it affects different areas of the organisation — and beyond.
Users
How big is the user base? How might the changes affect users? Which users? How many might be impacted? What are the consequences if your changes introduce critical bugs, and what is the plan for bug fixing or rolling back changes?
Longevity of the project
If the project is destined to be relatively short-lived, is it worth investing a significant amount of time for a technical migration?
Business needs
Which parts of your app will be affected? How frequent is your release pipeline? Do you have a lot of complex, high priority features planned? How will that be affected by pivoting developer resources?
Your team
How many developers are working on the codebase? How might their work be affected by a migration? Do you have a plan for how feature development and hotfixes will be integrated while the migration is taking place? What’s the learning curve for onboarding new developers? Have you considered any additional training needs for current developers?
When to do the migrationpermalink
Having decided that we wanted to migrate to Pinia at some point, the opportune time to do it was when we had a lull in the pace of feature development in order to focus on product strategy and gather customer feedback.
This is not always possible in a large project, however. If you need to continue with feature development in parallel with a migration, I would suggest keeping a record of new features (through Git or otherwise) and regularly reviewing this with your team to ensure that these features are migrated to the new framework at an appropriate time.
This clearly becomes more complex for large teams, and automation might help you here. Sophie Koonin delivered a great talk on large-scale technical migrations, which I highly recommend.
Differences between Vuex and Piniapermalink
If you’ve decided that migrating from Vuex to Pinia is the way to go, you should be aware of some of the key differences between the state management libraries.
Updating state with actions
In Vuex we have getters, actions and mutations. In Pinia, mutations no longer exist. The store can be updated directly with actions, which means it can be kept leaner.
- Code language
- js
// Vuex action someAction({ commit }) { commit('setUserID', id) } // Pinia action someAction({ commit }) { store.userID = id }
Pinia also allows us to update multiple properties in a store with $patch
:
- Code language
- js
store.$patch({ userID: id, username: name })
Using the store in a component
Using a store in a component allows us to access any property of that store directly. With Vuex we would dispatch and action like this:
- Code language
- js
// Vuex import { onBeforeMount } from 'vue' import { useStore } from 'vuex' const store = useStore() onBeforeMount(() => store.dispatch('someAction'))
With Pinia we can call that function directly:
- Code language
- js
// Pinia import { onBeforeMount } from 'vue' import { useStore } from './some-store' const store = useStore() onBeforeMount(() => store.someAction())
Dynamic modules
With Vuex, all store modules are loaded upfront unless we dynamically register them. Pinia stores are dynamic by default.
TypeScript support
I don’t use TypeScript, but if you do, making your state compatible with TypeScript is relatively straightforward with Pinia.
Options vs. Setup storespermalink
One of the most disconcerting things I found when migrating from Vuex to Pinia is that there are two different ways to define a store. The first way is very similar to Vuex, in the style of the Options API. Note that the store context is not exposed as an argument in our actions, but instead we use the this
keyword to get values from the store and to mutate state:
- Code language
- js
// Options store import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => { return { id: 1, username: 'Shirley', email: '[email protected]', alertSubscriptions: ['turbine_01', 'turbine_02'] error: null } }, getters: { subscriptionsUrl: (state) => { const params = new URLSearchParams({ user: state.id }) return `https://my-api.com?${params.toString()}` } }, actions: { async fetchAlertSubscriptions() { return fetch(this.subscriptionsUrl) .then((response) => response.json()) .then((data) => { // Mutate the state directly in the action this.alertSubscriptions = data }) .catch((error) => { this.error = error }) }, }, })
This style felt more intuitive to me when migrating from Vuex. However, Setup stores feel more in-tune with Vue’s Composition API, and make it easier to import different stores, allowing for greater flexibility. Here is a Setup store with the same properties for comparison. We define state and getters using refs and computed properties, which will be familiar if you already use the Composition API.
- Code language
- js
// Setup store import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useUserStore = defineStore('user', () => { const id = ref(1) const username = ref('Shirley') const email = ref('[email protected]') const alertSubscriptions = ['turbine_01', 'turbine_02'] const error = ref(null) const subscriptionsUrl = computed(() => { const params = new URLSearchParams({ user: state.id }) return `https://my-api.com?${params.toString()}` }) async function fetchAlertSubscriptions() { return fetch(subscriptionsUrl.value) .then((response) => response.json()) .then((data) => { // Mutate the state directly in the action alertSubscriptions.value = data }) .catch((e) => { error.value = e }) } return { id, username, email, alertSubscriptions, error } })
Just like the Composition API, Setup stores are more composable. We can quite easily import stores for use in other stores.
- Code language
- js
// Setup store import { ref, computed } from 'vue import { defineStore } from 'pinia' import { useUserStore } from './user' export const useSettingsStore = defineStore('settings', () => { // Give us access to the User store const userStore = useUserStore() const signedIn = computed(() => !!userStore.id) return { signedIn } }
In this project we ended up using both styles of store, which works fine but isn’t ideal. Having now spent a while getting comfortable with Pinia, my preference is now to default to Setup stores, with the very big caveat that stores are kept deliberately small, as we’ll soon see.
Small stores
An advantage of using Options stores is their built-in $reset
method, which makes it simple to reset stores to their initial state — for example, when a user signs out.
- Code language
- js
// Component using Options store async function signOutAndReset() { try { await signOut() userStore.$reset() // Reset the store to initial state } catch (error) { console.error(error) } }
If we want to do something like this we need to create our own $reset
method when we define the store:
- Code language
- js
// Setup store import { ref } from 'vue import { defineStore } from 'pinia' import INITIAL_STATE from './intial-state' export const useUserStore = defineStore('user', () => { const username = ref(INITIAL_STATE.username) function $reset() { username.value = INITIAL_STATE.username } return { username, reset } }
Then we can call it as $reset()
within a component just the same as above.
Be aware, if your store’s state consists of a number of properties, the $reset
method could also become very long, and hard to maintain. Additionally, unlike the Options store, we also have to remember to return the properties and methods we want to expose. I would recommend keeping stores small and modular to make this more manageable.
Store architecture
During the migration process I ended up with some very large stores as a result of the previous application architecture. With Vuex, my application store was split into modules. These modules were made even more manageable by keeping separate files for getters, actions and mutations.
- Code language
- plaintext
store ├─ index.js └─ modules └─ alerts ├─ index.js ├─ intial-state.js ├─ actions.js ├─ getters.js ├─ mutations.js └─ data-explorer └─ global └─ user
With Pinia, this isn’t really possible. State, actions and getters for a store need to live in the same file. Taking the time to split some of the larger modules into smaller stores was one of the most time-consuming and bug-ridden parts of the migration, but absolutely worth doing. It also gave me the chance to spend some time refactoring and removing redundant code.
Now my store architecture is much flatter, but composed of smaller files:
- Code language
- plaintext
store ├─ index.js ├─ initial-state.js ├─ alerts.js ├─ data-explorer.js ├─ global.js ├─ overview.js ├─ user.js ├─ settings.js ├─ workbench.js
Conclusionspermalink
Migrating from Vuex to Pinia was absolutely the right decision for our project, resulting in a simpler workflow and less code shipped overall. Whether it’s right for you will depend on the factors discussed above. My recommendations would be:
- Get familiar with the Composition API first
- Try out Pinia on a small project or test case before jumping into a large migration
- Plan your migration, taking into consideration user, business and developer needs, as well as risks
- Make sure all developers are aware of their responsibilities, and check in regularly
- If you plan to use Pinia in parallel with Vuex, make sure your stores are modular to begin with
- Keep stores small and composable
- Don’t over-complicate your stores. You don’t need actions for everything when you can update state directly in components
- Test thoroughly, and have a plan for fixing migration bugs and/or rolling back
I hope this is helpful if you’re considering a similar step for your Vue project!