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.