Skip to content

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 ​

bash
npm install pinia
# or
yarn add pinia
# or
pnpm add pinia

2. Configure Both Vuex and Pinia ​

ts
// 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 ​

ts
// 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.

ts
// 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.

ts
// 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
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.

ts
// 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.

ts
// 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 ​

ts
// 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 ​

vue
<!-- 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>
vue
<!-- 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 ​

vue
<!-- 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 ​

ts
// 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 ​

ts
// 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 ​

ts
// 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 ​

ts
// 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.

vue
<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:

ts
// 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 ​

  1. Start with the simplest modules
  2. Prioritize new features
  3. 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

Released under the MIT License.