Skip to content

DevTools

Pinia provides excellent debugging capabilities through Vue DevTools and other development tools. This guide covers how to effectively use these tools for debugging and developing Pinia applications.

Vue DevTools

Installation

Browser Extension:

Standalone Application:

bash
npm install -g @vue/devtools
# or
yarn global add @vue/devtools

# Run
vue-devtools

Pinia Integration

Pinia automatically integrates with Vue DevTools when both are available:

ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

// DevTools integration is automatic
app.use(pinia)
app.mount('#app')

DevTools Features

1. Store Inspector

View Store State:

ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const preferences = ref({
    theme: 'light',
    language: 'en'
  })
  
  return { user, preferences }
})

In DevTools:

  • Navigate to "Pinia" tab
  • Select store from the list
  • Inspect current state values
  • View nested objects and arrays

2. State Mutations

Track State Changes:

ts
// stores/counter.ts
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  
  const increment = () => {
    count.value++ // This mutation will be tracked
  }
  
  const setCount = (newCount: number) => {
    count.value = newCount // This mutation will be tracked
  }
  
  return { count, increment, setCount }
})

DevTools shows:

  • Mutation timeline
  • Before/after state values
  • Action that triggered the mutation
  • Stack trace

3. Action Tracking

Monitor Action Execution:

ts
// stores/api.ts
export const useApiStore = defineStore('api', () => {
  const data = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch('/api/data')
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  return { data, loading, error, fetchData }
})

DevTools displays:

  • Action execution timeline
  • Action parameters
  • Execution duration
  • Success/error status
  • State changes during action

4. Time Travel Debugging

Navigate Through State History:

ts
// Enable time travel (automatic in development)
const pinia = createPinia()

// In DevTools:
// 1. View mutation history
// 2. Click on any mutation to travel back
// 3. See application state at that point
// 4. Continue from that state or reset

DevTools Configuration

Custom Store Names

ts
// Better store identification in DevTools
export const useUserStore = defineStore('user-management', () => {
  // Store implementation
})

export const useShoppingCartStore = defineStore('shopping-cart', () => {
  // Store implementation
})

Store Grouping

ts
// Group related stores
export const useUserProfileStore = defineStore('user/profile', () => {
  // User profile store
})

export const useUserSettingsStore = defineStore('user/settings', () => {
  // User settings store
})

export const useProductCatalogStore = defineStore('product/catalog', () => {
  // Product catalog store
})

export const useProductCartStore = defineStore('product/cart', () => {
  // Shopping cart store
})

Development Tools

Hot Module Replacement (HMR)

Automatic Store Updates:

ts
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  
  const login = async (credentials) => {
    // Login logic
  }
  
  const logout = () => {
    user.value = null
  }
  
  return { user, login, logout }
})

// HMR preserves store state during development
// Changes to store logic are applied without losing state

Manual HMR Configuration:

ts
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  define: {
    __VUE_PROD_DEVTOOLS__: false, // Disable DevTools in production
  }
})

TypeScript Integration

Type-Safe Debugging:

ts
// stores/typed-store.ts
interface User {
  id: number
  name: string
  email: string
}

interface UserState {
  currentUser: User | null
  users: User[]
  loading: boolean
}

export const useUserStore = defineStore('user', (): UserState => {
  const currentUser = ref<User | null>(null)
  const users = ref<User[]>([])
  const loading = ref(false)
  
  return {
    currentUser: readonly(currentUser),
    users: readonly(users),
    loading: readonly(loading)
  }
})

// DevTools will show proper types for all state properties

Custom DevTools Plugin

Create Custom Inspector:

ts
// plugins/devtools-plugin.ts
import { App } from 'vue'
import { PiniaPluginContext } from 'pinia'

export function createDevToolsPlugin() {
  return ({ store, app }: PiniaPluginContext) => {
    // Add custom properties to DevTools
    store.$subscribe((mutation, state) => {
      console.group(`🍍 ${store.$id}`)
      console.log('Mutation:', mutation)
      console.log('State:', state)
      console.groupEnd()
    })
    
    // Add custom actions to DevTools
    store.$onAction(({ name, args, after, onError }) => {
      console.log(`🚀 Action "${name}" started with args:`, args)
      
      after((result) => {
        console.log(`✅ Action "${name}" completed with result:`, result)
      })
      
      onError((error) => {
        console.error(`❌ Action "${name}" failed:`, error)
      })
    })
  }
}

// main.ts
const pinia = createPinia()
pinia.use(createDevToolsPlugin())

Debugging Techniques

State Inspection

Direct State Access:

ts
// In browser console or debugger
const userStore = useUserStore()
console.log('Current user:', userStore.user)
console.log('All state:', userStore.$state)

// Reactive state watching
watch(
  () => userStore.user,
  (newUser, oldUser) => {
    console.log('User changed:', { newUser, oldUser })
  },
  { deep: true }
)

State Snapshots:

ts
// stores/debug.ts
export const useDebugStore = defineStore('debug', () => {
  const snapshots = ref([])
  
  const takeSnapshot = (label: string) => {
    const allStores = {}
    
    // Collect all store states
    const pinia = getActivePinia()
    pinia._s.forEach((store, id) => {
      allStores[id] = JSON.parse(JSON.stringify(store.$state))
    })
    
    snapshots.value.push({
      label,
      timestamp: Date.now(),
      states: allStores
    })
  }
  
  const compareSnapshots = (index1: number, index2: number) => {
    const snap1 = snapshots.value[index1]
    const snap2 = snapshots.value[index2]
    
    // Compare states and return differences
    return {
      snap1: snap1.label,
      snap2: snap2.label,
      differences: findDifferences(snap1.states, snap2.states)
    }
  }
  
  return { snapshots, takeSnapshot, compareSnapshots }
})

Action Debugging

Action Interceptors:

ts
// plugins/action-logger.ts
export function createActionLogger() {
  return ({ store }: PiniaPluginContext) => {
    store.$onAction(({ name, args, after, onError }) => {
      const startTime = Date.now()
      
      console.group(`🎬 Action: ${store.$id}.${name}`)
      console.log('Arguments:', args)
      console.log('State before:', JSON.stringify(store.$state))
      
      after((result) => {
        const duration = Date.now() - startTime
        console.log('State after:', JSON.stringify(store.$state))
        console.log('Result:', result)
        console.log(`Duration: ${duration}ms`)
        console.groupEnd()
      })
      
      onError((error) => {
        console.error('Error:', error)
        console.groupEnd()
      })
    })
  }
}

Async Action Debugging:

ts
// stores/async-debug.ts
export const useAsyncStore = defineStore('async', () => {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetchData = async (url: string) => {
    const actionId = `fetch-${Date.now()}`
    
    console.time(actionId)
    loading.value = true
    error.value = null
    
    try {
      console.log(`🌐 Fetching: ${url}`)
      const response = await fetch(url)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      data.value = await response.json()
      console.log(`✅ Fetch successful:`, data.value)
    } catch (err) {
      error.value = err.message
      console.error(`❌ Fetch failed:`, err)
    } finally {
      loading.value = false
      console.timeEnd(actionId)
    }
  }
  
  return { data, loading, error, fetchData }
})

Performance Debugging

Store Performance Monitor:

ts
// plugins/performance-monitor.ts
export function createPerformanceMonitor() {
  return ({ store }: PiniaPluginContext) => {
    const metrics = {
      mutations: 0,
      actions: 0,
      subscriptions: 0
    }
    
    // Track mutations
    store.$subscribe(() => {
      metrics.mutations++
    })
    
    // Track actions
    store.$onAction(() => {
      metrics.actions++
    })
    
    // Expose metrics
    store.$metrics = metrics
    
    // Log performance summary
    setInterval(() => {
      console.log(`📊 Store ${store.$id} metrics:`, metrics)
    }, 10000) // Every 10 seconds
  }
}

Memory Usage Tracking:

ts
// utils/memory-tracker.ts
export function trackMemoryUsage() {
  if (typeof window !== 'undefined' && 'performance' in window) {
    const memory = (performance as any).memory
    
    if (memory) {
      return {
        used: Math.round(memory.usedJSHeapSize / 1048576), // MB
        total: Math.round(memory.totalJSHeapSize / 1048576), // MB
        limit: Math.round(memory.jsHeapSizeLimit / 1048576) // MB
      }
    }
  }
  
  return null
}

// Usage in store
export const useMemoryStore = defineStore('memory', () => {
  const memoryUsage = ref(null)
  
  const updateMemoryUsage = () => {
    memoryUsage.value = trackMemoryUsage()
  }
  
  // Update every 5 seconds in development
  if (process.env.NODE_ENV === 'development') {
    setInterval(updateMemoryUsage, 5000)
  }
  
  return { memoryUsage, updateMemoryUsage }
})

Production Debugging

Error Tracking

Error Boundary for Stores:

ts
// plugins/error-tracker.ts
export function createErrorTracker() {
  return ({ store }: PiniaPluginContext) => {
    store.$onAction(({ name, onError }) => {
      onError((error) => {
        // Send to error tracking service
        if (typeof window !== 'undefined') {
          // Example: Sentry, LogRocket, etc.
          console.error(`Store action error in ${store.$id}.${name}:`, error)
          
          // Send to monitoring service
          // Sentry.captureException(error, {
          //   tags: {
          //     store: store.$id,
          //     action: name
          //   }
          // })
        }
      })
    })
  }
}

State Persistence for Debugging

Debug State Persistence:

ts
// plugins/debug-persistence.ts
export function createDebugPersistence() {
  return ({ store }: PiniaPluginContext) => {
    if (process.env.NODE_ENV === 'development') {
      // Save state to localStorage for debugging
      store.$subscribe((mutation, state) => {
        localStorage.setItem(
          `debug-${store.$id}`,
          JSON.stringify({
            state,
            timestamp: Date.now(),
            mutation
          })
        )
      })
      
      // Restore state from localStorage
      const savedState = localStorage.getItem(`debug-${store.$id}`)
      if (savedState) {
        try {
          const { state } = JSON.parse(savedState)
          store.$patch(state)
        } catch (error) {
          console.warn(`Failed to restore debug state for ${store.$id}:`, error)
        }
      }
    }
  }
}

Best Practices

DevTools Optimization

  1. Use Descriptive Store Names:
ts
// ❌ Bad
defineStore('store1', () => { /* ... */ })
defineStore('s', () => { /* ... */ })

// ✅ Good
defineStore('user-authentication', () => { /* ... */ })
defineStore('shopping-cart', () => { /* ... */ })
  1. Group Related Stores:
ts
// ✅ Good organization
defineStore('auth/user', () => { /* ... */ })
defineStore('auth/permissions', () => { /* ... */ })
defineStore('ecommerce/products', () => { /* ... */ })
defineStore('ecommerce/cart', () => { /* ... */ })
  1. Add Meaningful Action Names:
ts
// ❌ Bad
const doSomething = () => { /* ... */ }
const update = () => { /* ... */ }

// ✅ Good
const authenticateUser = () => { /* ... */ }
const updateUserProfile = () => { /* ... */ }

Development vs Production

ts
// main.ts
const pinia = createPinia()

if (process.env.NODE_ENV === 'development') {
  // Development-only plugins
  pinia.use(createActionLogger())
  pinia.use(createPerformanceMonitor())
  pinia.use(createDebugPersistence())
}

if (process.env.NODE_ENV === 'production') {
  // Production-only plugins
  pinia.use(createErrorTracker())
}

app.use(pinia)

Security Considerations

ts
// Avoid exposing sensitive data in DevTools
export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(null)
  
  // ❌ Don't expose sensitive data directly
  // return { user, token }
  
  // ✅ Use getters to control exposure
  const publicUser = computed(() => {
    if (!user.value) return null
    
    return {
      id: user.value.id,
      name: user.value.name,
      email: user.value.email
      // Don't expose sensitive fields
    }
  })
  
  const isAuthenticated = computed(() => !!token.value)
  
  return {
    user: publicUser,
    isAuthenticated
    // Don't expose token directly
  }
})

Troubleshooting

Common Issues

  1. DevTools Not Showing Pinia Tab:
ts
// Ensure Vue DevTools is installed and enabled
// Check browser console for errors
// Verify Pinia is properly installed

// Force DevTools detection
if (process.env.NODE_ENV === 'development') {
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = window.__VUE_DEVTOOLS_GLOBAL_HOOK__ || {}
}
  1. Store State Not Updating in DevTools:
ts
// Ensure you're using reactive references
const state = ref(initialValue) // ✅ Reactive
const state = initialValue // ❌ Not reactive

// Ensure proper state mutations
state.value = newValue // ✅ Triggers reactivity
state = newValue // ❌ Doesn't trigger reactivity
  1. Actions Not Appearing in Timeline:
ts
// Ensure actions are properly defined and returned
export const useStore = defineStore('store', () => {
  const action = () => {
    // Action logic
  }
  
  return {
    action // ✅ Must be returned to be tracked
  }
})

Performance Issues

ts
// Avoid excessive DevTools updates
export const useOptimizedStore = defineStore('optimized', () => {
  const data = ref([])
  
  // ❌ Triggers many DevTools updates
  const addItemsOneByOne = (items) => {
    items.forEach(item => {
      data.value.push(item) // Each push triggers update
    })
  }
  
  // ✅ Single DevTools update
  const addItemsBatch = (items) => {
    data.value = [...data.value, ...items]
  }
  
  return { data, addItemsBatch }
})

Released under the MIT License.