Vuex Migration
Comprehensive guide for migrating from Vuex to Pinia, including automated migration tools, step-by-step conversion process, and compatibility patterns for gradual migration.
Features
- 🔄 Automated migration tools and scripts
- 📋 Step-by-step migration checklist
- 🔀 Gradual migration strategies
- 🛠️ Vuex compatibility layer
- 📊 State structure conversion
- 🎯 Action and mutation mapping
- 🧪 Testing migration strategies
- 📦 Module system conversion
- 🔧 DevTools integration
- 📝 Migration validation tools
Migration Overview
Key Differences
Aspect | Vuex | Pinia |
---|---|---|
Store Definition | Single store with modules | Multiple stores |
Mutations | Required for state changes | Direct state mutation |
Actions | Async operations | Actions can be sync/async |
Getters | Computed properties | Computed properties |
Modules | Nested modules | Separate stores |
TypeScript | Complex typing | Built-in TypeScript support |
DevTools | Vue DevTools | Vue DevTools + better DX |
SSR | Manual setup | Automatic SSR support |
Migration Tools
Automated Migration Script
typescript
// scripts/vuex-to-pinia-migration.ts
import * as fs from 'fs'
import * as path from 'path'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generate from '@babel/generator'
import * as t from '@babel/types'
interface VuexModule {
name: string
state: any
mutations: Record<string, Function>
actions: Record<string, Function>
getters: Record<string, Function>
modules?: Record<string, VuexModule>
}
interface MigrationConfig {
sourceDir: string
targetDir: string
storeFile: string
preserveModules: boolean
generateTypes: boolean
createTests: boolean
}
class VuexToPiniaMigrator {
private config: MigrationConfig
private vuexStore: any
private migrationLog: string[] = []
constructor(config: MigrationConfig) {
this.config = config
}
async migrate(): Promise<void> {
console.log('🚀 Starting Vuex to Pinia migration...')
try {
// 1. Parse existing Vuex store
await this.parseVuexStore()
// 2. Create Pinia stores
await this.createPiniaStores()
// 3. Generate TypeScript types
if (this.config.generateTypes) {
await this.generateTypes()
}
// 4. Create tests
if (this.config.createTests) {
await this.createTests()
}
// 5. Generate migration report
await this.generateMigrationReport()
console.log('✅ Migration completed successfully!')
} catch (error) {
console.error('❌ Migration failed:', error)
throw error
}
}
private async parseVuexStore(): Promise<void> {
const storeFilePath = path.join(this.config.sourceDir, this.config.storeFile)
const storeContent = fs.readFileSync(storeFilePath, 'utf-8')
const ast = parse(storeContent, {
sourceType: 'module',
plugins: ['typescript', 'decorators-legacy']
})
this.vuexStore = this.extractStoreStructure(ast)
this.migrationLog.push(`✓ Parsed Vuex store from ${storeFilePath}`)
}
private extractStoreStructure(ast: any): VuexModule {
let storeStructure: VuexModule = {
name: 'root',
state: {},
mutations: {},
actions: {},
getters: {},
modules: {}
}
traverse(ast, {
ObjectExpression(path) {
const properties = path.node.properties
properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
const key = prop.key.name
switch (key) {
case 'state':
storeStructure.state = this.extractState(prop.value)
break
case 'mutations':
storeStructure.mutations = this.extractMutations(prop.value)
break
case 'actions':
storeStructure.actions = this.extractActions(prop.value)
break
case 'getters':
storeStructure.getters = this.extractGetters(prop.value)
break
case 'modules':
storeStructure.modules = this.extractModules(prop.value)
break
}
}
})
}
})
return storeStructure
}
private extractState(node: any): any {
// Extract state properties
const state: any = {}
if (t.isObjectExpression(node)) {
node.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
state[prop.key.name] = this.extractValue(prop.value)
}
})
} else if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) {
// Handle state as function
const returnStatement = node.body.body?.find((stmt: any) => t.isReturnStatement(stmt))
if (returnStatement && t.isObjectExpression(returnStatement.argument)) {
return this.extractState(returnStatement.argument)
}
}
return state
}
private extractMutations(node: any): Record<string, Function> {
const mutations: Record<string, Function> = {}
if (t.isObjectExpression(node)) {
node.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
mutations[prop.key.name] = prop.value
}
})
}
return mutations
}
private extractActions(node: any): Record<string, Function> {
const actions: Record<string, Function> = {}
if (t.isObjectExpression(node)) {
node.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
actions[prop.key.name] = prop.value
}
})
}
return actions
}
private extractGetters(node: any): Record<string, Function> {
const getters: Record<string, Function> = {}
if (t.isObjectExpression(node)) {
node.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
getters[prop.key.name] = prop.value
}
})
}
return getters
}
private extractModules(node: any): Record<string, VuexModule> {
const modules: Record<string, VuexModule> = {}
if (t.isObjectExpression(node)) {
node.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
modules[prop.key.name] = this.extractStoreStructure(prop.value)
}
})
}
return modules
}
private extractValue(node: any): any {
if (t.isStringLiteral(node)) return node.value
if (t.isNumericLiteral(node)) return node.value
if (t.isBooleanLiteral(node)) return node.value
if (t.isNullLiteral(node)) return null
if (t.isArrayExpression(node)) {
return node.elements.map((el: any) => this.extractValue(el))
}
if (t.isObjectExpression(node)) {
const obj: any = {}
node.properties.forEach((prop: any) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
obj[prop.key.name] = this.extractValue(prop.value)
}
})
return obj
}
return undefined
}
private async createPiniaStores(): Promise<void> {
// Create main store
await this.createMainStore(this.vuexStore)
// Create module stores
if (this.vuexStore.modules) {
for (const [moduleName, moduleData] of Object.entries(this.vuexStore.modules)) {
await this.createModuleStore(moduleName, moduleData)
}
}
}
private async createMainStore(storeData: VuexModule): Promise<void> {
const storeContent = this.generatePiniaStore('main', storeData)
const filePath = path.join(this.config.targetDir, 'stores', 'main.ts')
await this.ensureDirectoryExists(path.dirname(filePath))
fs.writeFileSync(filePath, storeContent)
this.migrationLog.push(`✓ Created main store: ${filePath}`)
}
private async createModuleStore(moduleName: string, moduleData: VuexModule): Promise<void> {
const storeContent = this.generatePiniaStore(moduleName, moduleData)
const filePath = path.join(this.config.targetDir, 'stores', `${moduleName}.ts`)
await this.ensureDirectoryExists(path.dirname(filePath))
fs.writeFileSync(filePath, storeContent)
this.migrationLog.push(`✓ Created ${moduleName} store: ${filePath}`)
}
private generatePiniaStore(storeName: string, storeData: VuexModule): string {
const stateProps = Object.keys(storeData.state)
const mutations = Object.keys(storeData.mutations)
const actions = Object.keys(storeData.actions)
const getters = Object.keys(storeData.getters)
return `import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Migrated from Vuex module: ${storeName}
export const use${this.capitalize(storeName)}Store = defineStore('${storeName}', () => {
// State (migrated from Vuex state)
${this.generateStateDeclarations(storeData.state)}
// Getters (migrated from Vuex getters)
${this.generateGetters(storeData.getters)}
// Actions (migrated from Vuex mutations and actions)
${this.generateActions(storeData.mutations, storeData.actions)}
return {
// State
${stateProps.map(prop => ` ${prop}`).join(',\n')},
// Getters
${getters.map(getter => ` ${getter}`).join(',\n')},
// Actions
${[...mutations, ...actions].map(action => ` ${action}`).join(',\n')}
}
})
// Type definitions
export interface ${this.capitalize(storeName)}State {
${this.generateStateInterface(storeData.state)}
}
`
}
private generateStateDeclarations(state: any): string {
return Object.entries(state)
.map(([key, value]) => {
const defaultValue = JSON.stringify(value)
return ` const ${key} = ref(${defaultValue})`
})
.join('\n')
}
private generateGetters(getters: Record<string, Function>): string {
return Object.entries(getters)
.map(([name, getter]) => {
// Convert Vuex getter to Pinia computed
const getterCode = this.convertVuexGetterToPinia(getter)
return ` const ${name} = computed(() => {\n ${getterCode}\n })`
})
.join('\n\n')
}
private generateActions(mutations: Record<string, Function>, actions: Record<string, Function>): string {
const mutationActions = Object.entries(mutations)
.map(([name, mutation]) => {
const actionCode = this.convertVuexMutationToPinia(mutation)
return ` function ${name}(payload?: any) {\n ${actionCode}\n }`
})
const regularActions = Object.entries(actions)
.map(([name, action]) => {
const actionCode = this.convertVuexActionToPinia(action)
return ` async function ${name}(payload?: any) {\n ${actionCode}\n }`
})
return [...mutationActions, ...regularActions].join('\n\n')
}
private generateStateInterface(state: any): string {
return Object.entries(state)
.map(([key, value]) => {
const type = this.inferType(value)
return ` ${key}: ${type}`
})
.join('\n')
}
private convertVuexGetterToPinia(getter: Function): string {
// This is a simplified conversion - in practice, you'd need more sophisticated AST manipulation
let code = getter.toString()
// Replace Vuex getter parameters with Pinia equivalents
code = code.replace(/\(state, getters, rootState, rootGetters\)/g, '()')
code = code.replace(/state\./g, '')
code = code.replace(/getters\./g, '')
return code
}
private convertVuexMutationToPinia(mutation: Function): string {
let code = mutation.toString()
// Replace Vuex mutation parameters
code = code.replace(/\(state, payload\)/g, '(payload)')
code = code.replace(/state\./g, '')
// Convert direct assignments to ref updates
code = code.replace(/(\w+)\s*=/g, '$1.value =')
return code
}
private convertVuexActionToPinia(action: Function): string {
let code = action.toString()
// Replace Vuex action parameters
code = code.replace(/\({ commit, dispatch, state, getters, rootState, rootGetters }, payload\)/g, '(payload)')
code = code.replace(/commit\(/g, 'this.')
code = code.replace(/dispatch\(/g, 'this.')
code = code.replace(/state\./g, 'this.')
return code
}
private inferType(value: any): string {
if (Array.isArray(value)) {
if (value.length > 0) {
const itemType = this.inferType(value[0])
return `${itemType}[]`
}
return 'any[]'
}
if (value === null) return 'null'
if (typeof value === 'object') return 'Record<string, any>'
return typeof value
}
private capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
private async ensureDirectoryExists(dirPath: string): Promise<void> {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
}
private async generateTypes(): Promise<void> {
const typesContent = this.generateGlobalTypes()
const filePath = path.join(this.config.targetDir, 'types', 'stores.ts')
await this.ensureDirectoryExists(path.dirname(filePath))
fs.writeFileSync(filePath, typesContent)
this.migrationLog.push(`✓ Generated types: ${filePath}`)
}
private generateGlobalTypes(): string {
return `// Auto-generated types from Vuex migration
export interface RootState {
// Add your root state types here
}
export interface StoreModules {
// Add your store module types here
}
// Re-export all store types
export * from '../stores/main'
${Object.keys(this.vuexStore.modules || {})
.map(moduleName => `export * from '../stores/${moduleName}'`)
.join('\n')}
`
}
private async createTests(): Promise<void> {
// Create test for main store
await this.createStoreTest('main', this.vuexStore)
// Create tests for module stores
if (this.vuexStore.modules) {
for (const [moduleName, moduleData] of Object.entries(this.vuexStore.modules)) {
await this.createStoreTest(moduleName, moduleData)
}
}
}
private async createStoreTest(storeName: string, storeData: VuexModule): Promise<void> {
const testContent = this.generateStoreTest(storeName, storeData)
const filePath = path.join(this.config.targetDir, 'tests', 'stores', `${storeName}.test.ts`)
await this.ensureDirectoryExists(path.dirname(filePath))
fs.writeFileSync(filePath, testContent)
this.migrationLog.push(`✓ Created test: ${filePath}`)
}
private generateStoreTest(storeName: string, storeData: VuexModule): string {
const storeHook = `use${this.capitalize(storeName)}Store`
return `import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { ${storeHook} } from '../../stores/${storeName}'
describe('${storeName} Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with correct default state', () => {
const store = ${storeHook}()
// Test initial state
${Object.entries(storeData.state)
.map(([key, value]) => ` expect(store.${key}).toEqual(${JSON.stringify(value)})`)
.join('\n')}
})
${Object.keys(storeData.mutations)
.map(mutationName => ` it('should handle ${mutationName} mutation', () => {
const store = ${storeHook}()
// TODO: Add test for ${mutationName}
// store.${mutationName}(payload)
// expect(store.someState).toBe(expectedValue)
})`)
.join('\n\n')}
${Object.keys(storeData.actions)
.map(actionName => ` it('should handle ${actionName} action', async () => {
const store = ${storeHook}()
// TODO: Add test for ${actionName}
// await store.${actionName}(payload)
// expect(store.someState).toBe(expectedValue)
})`)
.join('\n\n')}
${Object.keys(storeData.getters)
.map(getterName => ` it('should compute ${getterName} getter correctly', () => {
const store = ${storeHook}()
// TODO: Add test for ${getterName}
// expect(store.${getterName}).toBe(expectedValue)
})`)
.join('\n\n')}
})
`
}
private async generateMigrationReport(): Promise<void> {
const report = `# Vuex to Pinia Migration Report
Generated on: ${new Date().toISOString()}
## Migration Summary
${this.migrationLog.map(log => `- ${log}`).join('\n')}
## Manual Steps Required
1. Review generated stores and update business logic
2. Update component imports from Vuex to Pinia
3. Replace \`mapState\`, \`mapGetters\`, \`mapActions\` with Pinia equivalents
4. Update router guards and middleware
5. Test all functionality thoroughly
6. Update documentation
## Breaking Changes
- Mutations are now direct state modifications
- Actions can be synchronous or asynchronous
- No more \`commit\` and \`dispatch\` - call actions directly
- Module namespacing is replaced with separate stores
## Next Steps
1. Run tests: \`npm run test\`
2. Update components to use new stores
3. Remove Vuex dependencies
4. Update build configuration
`
const filePath = path.join(this.config.targetDir, 'MIGRATION_REPORT.md')
fs.writeFileSync(filePath, report)
console.log(`📋 Migration report generated: ${filePath}`)
}
}
// Usage
export async function migrateVuexToPinia(config: MigrationConfig) {
const migrator = new VuexToPiniaMigrator(config)
await migrator.migrate()
}
// CLI usage
if (require.main === module) {
const config: MigrationConfig = {
sourceDir: './src',
targetDir: './src',
storeFile: 'store/index.js',
preserveModules: true,
generateTypes: true,
createTests: true
}
migrateVuexToPinia(config).catch(console.error)
}
Vuex Compatibility Layer
typescript
// utils/vuex-compat.ts
import { inject, InjectionKey } from 'vue'
import type { Store } from 'pinia'
// Compatibility layer for gradual migration
export interface VuexCompatStore {
state: any
getters: any
commit: (type: string, payload?: any) => void
dispatch: (type: string, payload?: any) => Promise<any>
}
// Create a Vuex-like interface for Pinia stores
export function createVuexCompat(piniaStore: any): VuexCompatStore {
return {
get state() {
return piniaStore.$state
},
get getters() {
// Map computed properties as getters
const getters: any = {}
for (const key in piniaStore) {
if (typeof piniaStore[key] === 'function' && key.startsWith('get')) {
getters[key] = piniaStore[key]
}
}
return getters
},
commit(type: string, payload?: any) {
// Map mutations to direct method calls
if (typeof piniaStore[type] === 'function') {
piniaStore[type](payload)
} else {
console.warn(`Mutation ${type} not found in store`)
}
},
async dispatch(type: string, payload?: any) {
// Map actions to direct method calls
if (typeof piniaStore[type] === 'function') {
return await piniaStore[type](payload)
} else {
console.warn(`Action ${type} not found in store`)
return Promise.resolve()
}
}
}
}
// Vuex-style helpers for Pinia
export function mapState(storeId: string, states: string[]) {
return states.reduce((mapped, state) => {
mapped[state] = function(this: any) {
const store = this.$pinia.state.value[storeId]
return store ? store[state] : undefined
}
return mapped
}, {} as any)
}
export function mapGetters(storeId: string, getters: string[]) {
return getters.reduce((mapped, getter) => {
mapped[getter] = function(this: any) {
const store = this.$pinia._s.get(storeId)
return store ? store[getter] : undefined
}
return mapped
}, {} as any)
}
export function mapActions(storeId: string, actions: string[]) {
return actions.reduce((mapped, action) => {
mapped[action] = function(this: any, payload?: any) {
const store = this.$pinia._s.get(storeId)
return store ? store[action](payload) : Promise.resolve()
}
return mapped
}, {} as any)
}
export function mapMutations(storeId: string, mutations: string[]) {
return mutations.reduce((mapped, mutation) => {
mapped[mutation] = function(this: any, payload?: any) {
const store = this.$pinia._s.get(storeId)
if (store && typeof store[mutation] === 'function') {
store[mutation](payload)
}
}
return mapped
}, {} as any)
}
Step-by-Step Migration Examples
1. Simple Vuex Store
Before (Vuex):
javascript
// store/modules/counter.js
export default {
namespaced: true,
state: {
count: 0,
history: []
},
mutations: {
INCREMENT(state) {
state.count++
state.history.push(`Incremented to ${state.count}`)
},
DECREMENT(state) {
state.count--
state.history.push(`Decremented to ${state.count}`)
},
SET_COUNT(state, value) {
state.count = value
state.history.push(`Set to ${value}`)
}
},
actions: {
async incrementAsync({ commit }, delay = 1000) {
await new Promise(resolve => setTimeout(resolve, delay))
commit('INCREMENT')
},
reset({ commit }) {
commit('SET_COUNT', 0)
}
},
getters: {
doubleCount: state => state.count * 2,
isPositive: state => state.count > 0,
lastAction: state => state.history[state.history.length - 1]
}
}
After (Pinia):
typescript
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const history = ref<string[]>([])
// Getters
const doubleCount = computed(() => count.value * 2)
const isPositive = computed(() => count.value > 0)
const lastAction = computed(() => history.value[history.value.length - 1])
// Actions (mutations + actions combined)
function increment() {
count.value++
history.value.push(`Incremented to ${count.value}`)
}
function decrement() {
count.value--
history.value.push(`Decremented to ${count.value}`)
}
function setCount(value: number) {
count.value = value
history.value.push(`Set to ${value}`)
}
async function incrementAsync(delay = 1000) {
await new Promise(resolve => setTimeout(resolve, delay))
increment()
}
function reset() {
setCount(0)
}
return {
// State
count,
history,
// Getters
doubleCount,
isPositive,
lastAction,
// Actions
increment,
decrement,
setCount,
incrementAsync,
reset
}
})
2. Component Migration
Before (Vuex):
vue
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<p>Last Action: {{ lastAction }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementAsync">+ Async</button>
<button @click="reset">Reset</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
export default {
computed: {
...mapState('counter', ['count']),
...mapGetters('counter', ['doubleCount', 'lastAction'])
},
methods: {
...mapMutations('counter', ['increment', 'decrement']),
...mapActions('counter', ['incrementAsync', 'reset'])
}
}
</script>
After (Pinia):
vue
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<p>Last Action: {{ counter.lastAction }}</p>
<button @click="counter.increment">+</button>
<button @click="counter.decrement">-</button>
<button @click="counter.incrementAsync">+ Async</button>
<button @click="counter.reset">Reset</button>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
3. Complex Store with Modules
Before (Vuex):
javascript
// store/modules/user.js
export default {
namespaced: true,
state: {
currentUser: null,
preferences: {},
isAuthenticated: false
},
mutations: {
SET_USER(state, user) {
state.currentUser = user
state.isAuthenticated = !!user
},
SET_PREFERENCES(state, preferences) {
state.preferences = preferences
},
LOGOUT(state) {
state.currentUser = null
state.isAuthenticated = false
state.preferences = {}
}
},
actions: {
async login({ commit, dispatch }, credentials) {
try {
const user = await api.login(credentials)
commit('SET_USER', user)
await dispatch('loadPreferences')
return user
} catch (error) {
throw error
}
},
async loadPreferences({ commit, state }) {
if (state.currentUser) {
const preferences = await api.getUserPreferences(state.currentUser.id)
commit('SET_PREFERENCES', preferences)
}
},
logout({ commit }) {
commit('LOGOUT')
// Clear other stores
commit('posts/CLEAR_POSTS', null, { root: true })
}
},
getters: {
userName: state => state.currentUser?.name || 'Guest',
userRole: state => state.currentUser?.role || 'guest',
hasPermission: state => permission => {
return state.currentUser?.permissions?.includes(permission) || false
}
}
}
After (Pinia):
typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { usePostsStore } from './posts'
import * as api from '@/api'
interface User {
id: string
name: string
role: string
permissions: string[]
}
export const useUserStore = defineStore('user', () => {
// State
const currentUser = ref<User | null>(null)
const preferences = ref<Record<string, any>>({})
const isAuthenticated = ref(false)
// Getters
const userName = computed(() => currentUser.value?.name || 'Guest')
const userRole = computed(() => currentUser.value?.role || 'guest')
const hasPermission = computed(() => {
return (permission: string) => {
return currentUser.value?.permissions?.includes(permission) || false
}
})
// Actions
function setUser(user: User | null) {
currentUser.value = user
isAuthenticated.value = !!user
}
function setPreferences(newPreferences: Record<string, any>) {
preferences.value = newPreferences
}
async function login(credentials: { email: string; password: string }) {
try {
const user = await api.login(credentials)
setUser(user)
await loadPreferences()
return user
} catch (error) {
throw error
}
}
async function loadPreferences() {
if (currentUser.value) {
const userPreferences = await api.getUserPreferences(currentUser.value.id)
setPreferences(userPreferences)
}
}
function logout() {
setUser(null)
setPreferences({})
// Clear other stores
const postsStore = usePostsStore()
postsStore.clearPosts()
}
return {
// State
currentUser,
preferences,
isAuthenticated,
// Getters
userName,
userRole,
hasPermission,
// Actions
setUser,
setPreferences,
login,
loadPreferences,
logout
}
})
Migration Checklist
Pre-Migration
- [ ] Audit existing Vuex store structure
- [ ] Identify module dependencies
- [ ] Create backup of current implementation
- [ ] Set up Pinia in the project
- [ ] Plan migration strategy (gradual vs. complete)
During Migration
- [ ] Convert state to reactive refs
- [ ] Merge mutations and actions into actions
- [ ] Convert getters to computed properties
- [ ] Update module structure to separate stores
- [ ] Handle cross-store dependencies
- [ ] Update component imports and usage
Post-Migration
- [ ] Update all component imports
- [ ] Replace Vuex helpers with Pinia equivalents
- [ ] Update router guards and middleware
- [ ] Test all functionality
- [ ] Update documentation
- [ ] Remove Vuex dependencies
- [ ] Update build configuration
Testing Migration
typescript
// tests/migration.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { useUserStore } from '@/stores/user'
describe('Vuex to Pinia Migration', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
describe('Counter Store Migration', () => {
it('should maintain same functionality as Vuex version', () => {
const counter = useCounterStore()
// Test initial state
expect(counter.count).toBe(0)
expect(counter.history).toEqual([])
// Test mutations -> actions
counter.increment()
expect(counter.count).toBe(1)
expect(counter.history).toContain('Incremented to 1')
counter.decrement()
expect(counter.count).toBe(0)
expect(counter.history).toContain('Decremented to 0')
// Test getters
expect(counter.doubleCount).toBe(0)
expect(counter.isPositive).toBe(false)
})
it('should handle async actions correctly', async () => {
const counter = useCounterStore()
await counter.incrementAsync(100)
expect(counter.count).toBe(1)
})
})
describe('User Store Migration', () => {
it('should handle authentication flow', async () => {
const user = useUserStore()
expect(user.isAuthenticated).toBe(false)
expect(user.userName).toBe('Guest')
// Mock login
const mockUser = {
id: '1',
name: 'John Doe',
role: 'admin',
permissions: ['read', 'write']
}
user.setUser(mockUser)
expect(user.isAuthenticated).toBe(true)
expect(user.userName).toBe('John Doe')
expect(user.hasPermission('read')).toBe(true)
})
})
})
Best Practices
- Gradual Migration: Migrate one module at a time
- Maintain Compatibility: Use compatibility layer during transition
- Test Thoroughly: Ensure all functionality works after migration
- Update Documentation: Keep documentation current during migration
- Team Communication: Coordinate with team members on migration progress
- Performance Monitoring: Monitor performance before and after migration
- Rollback Plan: Have a plan to rollback if issues arise
- Training: Ensure team understands Pinia concepts and patterns
This migration guide provides a comprehensive approach to moving from Vuex to Pinia while maintaining application stability and functionality.