Skip to main content

Vue.js Vuex Plugins

Introduction

In the world of Vue.js state management, Vuex provides a robust foundation for managing application state. However, as applications grow in complexity, developers often need to extend Vuex's functionality in consistent ways across their stores. This is where Vuex plugins come into play.

Vuex plugins are a powerful yet often overlooked feature that allows you to extend Vuex's capabilities with additional functionality such as:

  • Logging state changes
  • Persisting state to local storage
  • Synchronizing state with a server
  • Adding analytics tracking
  • Creating middleware-like functionality

By the end of this tutorial, you'll understand how Vuex plugins work, how to create them, and how to leverage existing plugins to enhance your Vue applications.

What are Vuex Plugins?

Vuex plugins are simply functions that receive the store instance as the only argument. Within a plugin, you can hook into mutation events, dispatch actions, or even register dynamic modules.

A plugin's structure looks like this:

js
const myPlugin = (store) => {
// Plugin code here has access to the store instance
}

Plugins are added to Vuex when creating the store:

js
import { createStore } from 'vuex'
import myPlugin from './myPlugin'

const store = createStore({
state: { /* ... */ },
mutations: { /* ... */ },
actions: { /* ... */ },
plugins: [myPlugin]
})

Creating Your First Vuex Plugin

Let's create a simple logging plugin that logs every mutation to the console. This helps with debugging and understanding state changes:

js
// loggerPlugin.js
const loggerPlugin = (store) => {
// Subscribe to mutations
store.subscribe((mutation, state) => {
console.log('Mutation:', mutation.type)
console.log('Mutation payload:', mutation.payload)
console.log('Current state:', JSON.stringify(state, null, 2))
})
}

export default loggerPlugin

To use this plugin:

js
// store/index.js
import { createStore } from 'vuex'
import loggerPlugin from './plugins/loggerPlugin'

const store = createStore({
state: {
count: 0
},
mutations: {
increment(state, amount = 1) {
state.count += amount
}
},
plugins: [loggerPlugin]
})

export default store

Now, when you commit a mutation:

js
store.commit('increment', 5)

The console will show:

Mutation: increment
Mutation payload: 5
Current state: {
"count": 5
}

Plugin Hooks and API

Plugins can access several store methods and properties. The most commonly used are:

1. store.subscribe

Listens to mutations being committed:

js
store.subscribe((mutation, state) => {
// Called after every mutation
// mutation = { type, payload }
})

2. store.subscribeAction

Listens to actions being dispatched:

js
store.subscribeAction((action, state) => {
// Called when an action is dispatched
// action = { type, payload }
})

Since Vuex 4.0, you can also subscribe to actions before they are processed:

js
store.subscribeAction({
before: (action, state) => {
console.log(`Before action ${action.type}`)
},
after: (action, state) => {
console.log(`After action ${action.type}`)
}
})

3. store.registerModule and store.unregisterModule

Dynamically register or remove modules:

js
const dynamicModulePlugin = (store) => {
// Register a module when certain conditions are met
if (someCondition) {
store.registerModule('dynamic', {
state: { /* ... */ },
mutations: { /* ... */ }
})
}
}

Practical Plugin Examples

Let's explore some practical examples of Vuex plugins that you might use in real applications:

1. Local Storage Persistence Plugin

One common use for plugins is persisting Vuex state to localStorage, so state survives page reloads:

js
// persistencePlugin.js
const persistencePlugin = (store) => {
// Load saved state from localStorage when the plugin is initialized
const savedState = localStorage.getItem('vuex-state')
if (savedState) {
store.replaceState(JSON.parse(savedState))
}

// Subscribe to store mutations
store.subscribe((mutation, state) => {
// Save state to localStorage after each mutation
localStorage.setItem('vuex-state', JSON.stringify(state))
})
}

export default persistencePlugin

Usage:

js
// store/index.js
import persistencePlugin from './plugins/persistencePlugin'

const store = createStore({
// ... state, mutations, etc.
plugins: [persistencePlugin]
})

2. Analytics Tracking Plugin

Track specific mutations or actions for analytics purposes:

js
// analyticsPlugin.js
const analyticsPlugin = (store) => {
// Track specific mutations
store.subscribe((mutation, state) => {
switch (mutation.type) {
case 'addToCart':
trackEvent('Product Added', mutation.payload)
break
case 'completePurchase':
trackEvent('Purchase Completed', {
total: state.cart.total,
items: state.cart.items.length
})
break
}
})

// Function to send events to your analytics provider
function trackEvent(eventName, eventData) {
console.log(`Analytics: ${eventName}`, eventData)
// In a real app, you would send this to your analytics service
// analyticsService.track(eventName, eventData)
}
}

export default analyticsPlugin

3. Middleware Plugin

Create a middleware system similar to what's found in server frameworks:

js
// middlewarePlugin.js
const createMiddlewarePlugin = (middlewares) => {
return (store) => {
// Create a chain of middlewares
const chain = middlewares.map(middleware => middleware(store))

store.subscribeAction({
before: (action) => {
// Run all middlewares before action
chain.forEach(middleware => {
if (middleware.before) {
middleware.before(action)
}
})
},
after: (action) => {
// Run all middlewares after action
chain.forEach(middleware => {
if (middleware.after) {
middleware.after(action)
}
})
}
})
}
}

// Example middleware
const loggerMiddleware = (store) => ({
before: (action) => {
console.log(`Before action: ${action.type}`)
},
after: (action) => {
console.log(`After action: ${action.type}`)
}
})

export { createMiddlewarePlugin, loggerMiddleware }

Usage:

js
import { createMiddlewarePlugin, loggerMiddleware } from './plugins/middlewarePlugin'

// Define authentication middleware
const authMiddleware = (store) => ({
before: (action) => {
if (action.type.startsWith('user/') && !store.state.auth.isLoggedIn) {
console.warn('User must be logged in to perform this action')
throw new Error('Authentication required')
}
}
})

const middlewarePlugin = createMiddlewarePlugin([
loggerMiddleware,
authMiddleware
])

const store = createStore({
// ... state, mutations, etc.
plugins: [middlewarePlugin]
})

Understanding Plugin Order and Composition

When you use multiple plugins, they are applied in the order they are listed in the plugins array:

js
const store = createStore({
// ...
plugins: [
plugin1, // Applied first
plugin2, // Applied second
plugin3 // Applied third
]
})

This order can be important, especially if plugins depend on each other or modify the same parts of the store.

Plugin Composition Pattern

For more complex scenarios, you can create a plugin factory that composes multiple plugins:

js
// composePlugins.js
export function composePlugins(...plugins) {
return (store) => {
plugins.forEach(plugin => plugin(store))
}
}

Usage:

js
import { composePlugins } from './plugins/composePlugins'
import loggerPlugin from './plugins/loggerPlugin'
import persistencePlugin from './plugins/persistencePlugin'

// Create a combined plugin with specific settings
const enhancedPersistencePlugin = (store) => {
// Custom logic before applying persistence
console.log('Initializing enhanced persistence')
persistencePlugin(store)
}

const combinedPlugin = composePlugins(
loggerPlugin,
enhancedPersistencePlugin
)

const store = createStore({
// ...
plugins: [combinedPlugin]
})

Several well-maintained Vuex plugin libraries offer advanced functionality with minimal setup:

1. vuex-persist

For more advanced state persistence with customization options:

js
import VuexPersistence from 'vuex-persist'

const vuexLocal = new VuexPersistence({
storage: window.localStorage,
// Only persist specific modules
modules: ['user', 'cart']
})

const store = createStore({
// ...
plugins: [vuexLocal.plugin]
})

2. vuex-router-sync

Sync router state with the Vuex store:

js
import { createRouter } from 'vue-router'
import { createStore } from 'vuex'
import { sync } from 'vuex-router-sync'

const router = createRouter({ /* ... */ })
const store = createStore({ /* ... */ })

// Sync router with store
sync(store, router)

// Access route info from store state
// store.state.route.path
// store.state.route.params

Creating a Plugin with TypeScript

For TypeScript users, here's how to create a properly typed Vuex plugin:

ts
import { Store } from 'vuex'

interface State {
count: number
user: {
name: string
loggedIn: boolean
}
}

const typedPlugin = (store: Store<State>) => {
store.subscribe((mutation, state) => {
// TypeScript now knows the shape of your state
console.log(state.user.loggedIn)

// You can also check mutation types
if (mutation.type === 'setUser') {
const payload = mutation.payload as { name: string, loggedIn: boolean }
console.log(`User ${payload.name} logged in: ${payload.loggedIn}`)
}
})
}

export default typedPlugin

Best Practices for Vuex Plugins

  1. Keep plugins focused: Each plugin should have a single responsibility.

  2. Make plugins configurable: Use factory functions to create configurable plugins.

    js
    // Instead of this:
    const myPlugin = (store) => { /* ... */ }

    // Do this:
    const createMyPlugin = (options = {}) => {
    return (store) => {
    // Use options to customize behavior
    // ...
    }
    }

    // Usage:
    const myPlugin = createMyPlugin({ debug: true })
  3. Don't mutate state directly: Plugins should respect Vuex's mutation system.

    js
    // Bad
    const badPlugin = (store) => {
    store.state.count = 100 // Don't do this!
    }

    // Good
    const goodPlugin = (store) => {
    store.commit('setCount', 100)
    }
  4. Avoid excessive subscriptions: Too many subscription callbacks can impact performance.

  5. Handle plugin errors: Wrap plugin code in try/catch blocks to prevent one plugin from breaking others.

    js
    const safePlugin = (store) => {
    store.subscribe((mutation, state) => {
    try {
    // Plugin logic that might throw
    } catch (error) {
    console.error('Plugin error:', error)
    }
    })
    }

Debugging Plugins

When developing plugins, it's helpful to include debug options:

js
const createDebugPlugin = (options = {}) => {
const { enabled = true, verbose = false } = options

return (store) => {
if (!enabled) return

store.subscribe((mutation, state) => {
const message = `Mutation: ${mutation.type}`
console.log(message)

if (verbose) {
console.log('Payload:', mutation.payload)
console.log('New state:', state)
}
})
}
}

// Usage
const debugPlugin = createDebugPlugin({
enabled: process.env.NODE_ENV !== 'production',
verbose: true
})

Summary

Vuex plugins provide a powerful way to extend and enhance your Vue.js state management. They allow you to:

  • Add reusable, cross-cutting functionality to your store
  • Hook into the mutation and action lifecycles
  • Create advanced features like state persistence, logging, or authentication middleware
  • Keep your store modules focused while moving peripheral concerns to plugins

By leveraging plugins, you can create more maintainable Vuex stores with clean separation of concerns. The plugin system's simple but powerful design makes it appropriate for both small enhancements and complex store augmentations.

Additional Resources

To continue learning about Vuex plugins:

Exercises

  1. Create a logging plugin that only logs mutations for specific modules.

  2. Develop a "time travel" plugin that records state history and allows you to revert to previous states.

  3. Create a plugin that synchronizes specific parts of your Vuex store with a WebSocket service.

  4. Build a plugin that tracks and limits the rate at which certain mutations can be committed (e.g., to prevent spam clicks).

  5. Implement a plugin that automatically saves form data to localStorage as a user types, with debouncing.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)