Vuex Compatibility Guide
This guide helps you gradually introduce Pinia into existing Vuex projects for smooth migration without breaking existing functionality.
Overview
Pinia and Vuex can coexist in the same project, allowing you to:
- Gradually migrate existing Vuex modules
- Use Pinia for new features
- Maintain application stability during migration
- Test and validate migration effects
Setting Up Coexistence Environment
1. Install Pinia
npm install pinia
# or
yarn add pinia
# or
pnpm add pinia
2. Configure Both Vuex and Pinia
// main.ts
import { createApp } from 'vue'
import { createStore } from 'vuex'
import { createPinia } from 'pinia'
import App from './App.vue'
// Existing Vuex store
const vuexStore = createStore({
// Your existing Vuex configuration
modules: {
user: userModule,
products: productsModule,
// ... other modules
}
})
// New Pinia instance
const pinia = createPinia()
const app = createApp(App)
// Use both state management libraries
app.use(vuexStore)
app.use(pinia)
app.mount('#app')
3. TypeScript Configuration
// types/store.ts
import type { Store } from 'vuex'
import type { Pinia } from 'pinia'
// Extend ComponentCustomProperties
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<any>
$pinia: Pinia
}
}
Progressive Migration Strategies
Strategy 1: Module-by-Module Migration
Convert Vuex modules to Pinia stores one by one.
// Original Vuex module
// store/modules/cart.js
const cartModule = {
namespaced: true,
state: {
items: [],
total: 0
},
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.total
},
mutations: {
ADD_ITEM(state, item) {
state.items.push(item)
state.total += item.price
},
REMOVE_ITEM(state, itemId) {
const index = state.items.findIndex(item => item.id === itemId)
if (index > -1) {
state.total -= state.items[index].price
state.items.splice(index, 1)
}
}
},
actions: {
addItem({ commit }, item) {
commit('ADD_ITEM', item)
},
removeItem({ commit }, itemId) {
commit('REMOVE_ITEM', itemId)
}
}
}
// Convert to Pinia store
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed, readonly } from 'vue'
export interface CartItem {
id: string
name: string
price: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
// State
const items = ref<CartItem[]>([])
const total = ref(0)
// Getters
const itemCount = computed(() => items.value.length)
const totalPrice = computed(() => total.value)
// Actions
function addItem(item: CartItem) {
items.value.push(item)
total.value += item.price
}
function removeItem(itemId: string) {
const index = items.value.findIndex(item => item.id === itemId)
if (index > -1) {
total.value -= items.value[index].price
items.value.splice(index, 1)
}
}
return {
// State
items: readonly(items),
total: readonly(total),
// Getters
itemCount,
totalPrice,
// Actions
addItem,
removeItem
}
})
Strategy 2: Feature-based Migration
Use Pinia for new features while keeping existing functionality on Vuex.
// New features use Pinia
// stores/notifications.ts
export const useNotificationsStore = defineStore('notifications', () => {
const notifications = ref<Notification[]>([])
function addNotification(notification: Notification) {
notifications.value.push({
...notification,
id: Date.now().toString(),
timestamp: new Date()
})
}
function removeNotification(id: string) {
const index = notifications.value.findIndex(n => n.id === id)
if (index > -1) {
notifications.value.splice(index, 1)
}
}
return {
notifications: readonly(notifications),
addNotification,
removeNotification
}
})
// Use both in components
// components/Header.vue
<template>
<header>
<!-- Vuex data -->
<div>User: {{ $store.getters['user/fullName'] }}</div>
<!-- Pinia data -->
<div>Notifications: {{ notificationsStore.notifications.length }}</div>
</header>
</template>
<script setup lang="ts">
import { useNotificationsStore } from '@/stores/notifications'
const notificationsStore = useNotificationsStore()
</script>
Compatibility Tools
1. State Synchronization Tools
During migration, you might need to synchronize certain states between Vuex and Pinia.
// utils/state-sync.ts
import { watch } from 'vue'
import type { Store } from 'vuex'
import type { StoreGeneric } from 'pinia'
export function syncVuexToPinia<T>(
vuexStore: Store<any>,
vuexPath: string,
piniaStore: StoreGeneric,
piniaProperty: keyof T
) {
// Watch Vuex state changes and sync to Pinia
watch(
() => vuexStore.getters[vuexPath] || vuexStore.state[vuexPath],
(newValue) => {
(piniaStore as any)[piniaProperty] = newValue
},
{ immediate: true }
)
}
export function syncPiniaToVuex<T>(
piniaStore: StoreGeneric,
piniaProperty: keyof T,
vuexStore: Store<any>,
vuexMutation: string
) {
// Watch Pinia state changes and sync to Vuex
watch(
() => (piniaStore as any)[piniaProperty],
(newValue) => {
vuexStore.commit(vuexMutation, newValue)
}
)
}
// Usage example
// In component or plugin
syncVuexToPinia(
vuexStore,
'user/currentUser',
userPiniaStore,
'currentUser'
)
2. Unified Access Interface
Create a unified interface to access both Vuex and Pinia states.
// composables/useUnifiedStore.ts
import { computed } from 'vue'
import { useStore } from 'vuex'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
export function useUnifiedStore() {
const vuexStore = useStore()
const userStore = useUserStore()
const cartStore = useCartStore()
return {
// Vuex state (being migrated)
products: computed(() => vuexStore.state.products.items),
productsLoading: computed(() => vuexStore.state.products.loading),
// Pinia state (migrated)
user: computed(() => userStore.user),
cartItems: computed(() => cartStore.items),
// Unified actions
async fetchProducts() {
return vuexStore.dispatch('products/fetchProducts')
},
async login(credentials: LoginCredentials) {
return userStore.login(credentials)
},
addToCart(item: CartItem) {
cartStore.addItem(item)
}
}
}
3. Migration Helper
// utils/migration-helper.ts
export class MigrationHelper {
private migratedModules = new Set<string>()
markAsMigrated(moduleName: string) {
this.migratedModules.add(moduleName)
console.log(`✅ Module "${moduleName}" has been migrated to Pinia`)
}
isMigrated(moduleName: string): boolean {
return this.migratedModules.has(moduleName)
}
getMigrationStatus() {
return {
migrated: Array.from(this.migratedModules),
total: this.migratedModules.size
}
}
// Create migration check decorator
createMigrationCheck(moduleName: string) {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value
descriptor.value = function(...args: any[]) {
if (!this.isMigrated(moduleName)) {
console.warn(`⚠️ Module "${moduleName}" has not been migrated to Pinia yet`)
}
return originalMethod.apply(this, args)
}
}
}
}
export const migrationHelper = new MigrationHelper()
Component Migration Patterns
1. Progressive Component Migration
<!-- Before migration: Using Vuex -->
<template>
<div>
<h1>{{ fullName }}</h1>
<p>Cart items: {{ cartItemCount }}</p>
<button @click="logout">Logout</button>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('user', ['fullName']),
...mapGetters('cart', ['itemCount'])
},
methods: {
...mapActions('user', ['logout'])
}
}
</script>
<!-- After migration: Using Pinia -->
<template>
<div>
<h1>{{ userStore.fullName }}</h1>
<p>Cart items: {{ cartStore.itemCount }}</p>
<button @click="userStore.logout">Logout</button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
</script>
2. Mixed Usage Pattern
<!-- Transition period: Using both Vuex and Pinia -->
<template>
<div>
<!-- Using Pinia (migrated) -->
<h1>{{ userStore.fullName }}</h1>
<!-- Using Vuex (pending migration) -->
<div>Product count: {{ $store.getters['products/totalCount'] }}</div>
<button @click="handleAction">Execute Action</button>
</div>
</template>
<script setup lang="ts">
import { useStore } from 'vuex'
import { useUserStore } from '@/stores/user'
const vuexStore = useStore()
const userStore = useUserStore()
function handleAction() {
// Call both Vuex and Pinia actions
vuexStore.dispatch('products/fetchProducts')
userStore.updateLastActivity()
}
</script>
Testing Strategies
1. Parallel Testing
// tests/migration.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createStore } from 'vuex'
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '@/stores/cart'
import cartModule from '@/store/modules/cart'
describe('Cart Migration Tests', () => {
let vuexStore: any
let piniaStore: any
beforeEach(() => {
// Setup Vuex
vuexStore = createStore({
modules: {
cart: cartModule
}
})
// Setup Pinia
setActivePinia(createPinia())
piniaStore = useCartStore()
})
it('should produce same results in both implementations', () => {
const testItem = { id: '1', name: 'Test Item', price: 100, quantity: 1 }
// Vuex operations
vuexStore.dispatch('cart/addItem', testItem)
// Pinia operations
piniaStore.addItem(testItem)
// Verify consistent results
expect(vuexStore.getters['cart/itemCount']).toBe(piniaStore.itemCount)
expect(vuexStore.getters['cart/totalPrice']).toBe(piniaStore.totalPrice)
})
})
2. Compatibility Testing
// tests/compatibility.test.ts
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import { createPinia } from 'pinia'
import TestComponent from '@/components/TestComponent.vue'
describe('Vuex-Pinia Compatibility', () => {
it('should be able to use both Vuex and Pinia simultaneously', () => {
const vuexStore = createStore({
state: { message: 'Hello from Vuex' }
})
const pinia = createPinia()
const wrapper = mount(TestComponent, {
global: {
plugins: [vuexStore, pinia]
}
})
expect(wrapper.vm.$store).toBeDefined()
expect(wrapper.vm.$pinia).toBeDefined()
})
})
Performance Considerations
1. Memory Usage
// utils/memory-monitor.ts
export function monitorMemoryUsage() {
if (process.env.NODE_ENV === 'development') {
setInterval(() => {
if (performance.memory) {
console.log('Memory usage:', {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + 'MB',
limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
})
}
}, 10000) // Check every 10 seconds
}
}
2. Bundle Size Optimization
// Dynamic imports to reduce initial bundle size
// router/index.ts
const routes = [
{
path: '/legacy',
component: () => import('@/views/LegacyView.vue'), // Uses Vuex
},
{
path: '/modern',
component: () => import('@/views/ModernView.vue'), // Uses Pinia
}
]
Migration Checklist
Preparation Phase
- [ ] Install Pinia
- [ ] Configure Vuex and Pinia coexistence
- [ ] Set up TypeScript types
- [ ] Create migration plan
Migration Phase
- [ ] Choose migration strategy (by module or by feature)
- [ ] Convert first Vuex module
- [ ] Update related components
- [ ] Write tests to verify functionality
- [ ] Repeat for other modules
Validation Phase
- [ ] Run all tests
- [ ] Check performance impact
- [ ] Verify DevTools functionality
- [ ] Test SSR (if applicable)
Cleanup Phase
- [ ] Remove unused Vuex code
- [ ] Update documentation
- [ ] Uninstall Vuex dependency
- [ ] Final testing
Common Questions
Q: Can I use both Vuex and Pinia in the same component?
A: Yes, absolutely. This is a core feature of progressive migration.
<script setup>
import { useStore } from 'vuex'
import { useUserStore } from '@/stores/user'
const vuexStore = useStore()
const piniaStore = useUserStore()
</script>
Q: How do I handle Vuex plugins?
A: Convert Vuex plugins to Pinia plugins:
// Vuex plugin
const vuexPlugin = (store) => {
store.subscribe((mutation, state) => {
console.log(mutation.type)
})
}
// Pinia plugin
const piniaPlugin = ({ store }) => {
store.$subscribe((mutation, state) => {
console.log(mutation.storeId)
})
}
Q: Will migration affect performance?
A: There might be slight performance overhead in the short term (running two state management libraries), but Pinia performs better in the long run.
Q: How do I handle Vuex strict mode?
A: Pinia allows direct state mutations by default, so strict mode isn't needed. If you need similar functionality, use development tools for monitoring.
Best Practices
1. Migration Order
- Start with the simplest modules
- Prioritize new features
- Migrate core business logic last
2. Code Organization
src/
├── store/ # Vuex (gradually remove)
│ ├── modules/
│ └── index.ts
├── stores/ # Pinia (new)
│ ├── user.ts
│ ├── cart.ts
│ └── index.ts
└── composables/ # Unified interface
└── useUnifiedStore.ts
3. Documentation Updates
- Record migration progress
- Update API documentation
- Provide migration guide for team