Skip to content

Plugin Development

This guide will teach you how to develop custom plugins for Pinia, from basic concepts to advanced patterns.

What are Pinia Plugins?

Pinia plugins are functions that extend the functionality of stores. They can:

  • Add properties to stores
  • Add new methods to stores
  • Wrap existing methods
  • Change or extend store options
  • Add global functionality

Basic Plugin Structure

Plugin Function Signature

ts
type PiniaPlugin = (context: PiniaPluginContext) => 
  Partial<PiniaCustomProperties & PiniaCustomStateProperties> | void

Plugin Context

ts
interface PiniaPluginContext {
  pinia: Pinia          // Pinia instance
  app: App              // Vue app instance
  store: Store          // Current store instance
  options: StoreOptions // Store definition options
}

Basic Plugin Example

js
function myFirstPlugin({ store }) {
  // Add a property to every store
  return {
    hello: 'world'
  }
}

// Register the plugin
const pinia = createPinia()
pinia.use(myFirstPlugin)

// Now every store has a 'hello' property
const store = useMyStore()
console.log(store.hello) // 'world'

Development Environment Setup

Project Structure

my-pinia-plugin/
├── src/
│   ├── index.ts          # Main plugin file
│   ├── types.ts          # TypeScript definitions
│   └── utils.ts          # Utility functions
├── tests/
│   ├── plugin.test.ts    # Plugin tests
│   └── setup.ts          # Test setup
├── examples/
│   └── basic-usage.ts    # Usage examples
├── package.json
├── tsconfig.json
└── README.md

Package.json Setup

json
{
  "name": "pinia-my-plugin",
  "version": "1.0.0",
  "description": "A custom Pinia plugin",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rollup -c",
    "test": "vitest",
    "dev": "rollup -c -w"
  },
  "peerDependencies": {
    "pinia": "^2.0.0",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@rollup/plugin-typescript": "^11.0.0",
    "pinia": "^2.0.0",
    "rollup": "^3.0.0",
    "typescript": "^4.9.0",
    "vitest": "^0.28.0",
    "vue": "^3.2.0"
  }
}

Plugin Development Patterns

1. Adding Properties

js
function addPropertiesPlugin() {
  return {
    // Add reactive property
    timestamp: ref(Date.now()),
    
    // Add computed property
    formattedTime: computed(() => {
      return new Date(timestamp.value).toLocaleString()
    }),
    
    // Add method
    updateTimestamp() {
      timestamp.value = Date.now()
    }
  }
}

2. Wrapping Actions

js
function actionWrapperPlugin({ store }) {
  // Store original actions
  const originalActions = {}
  
  Object.keys(store).forEach(key => {
    if (typeof store[key] === 'function') {
      originalActions[key] = store[key]
      
      // Wrap the action
      store[key] = function(...args) {
        console.log(`Action ${key} called with:`, args)
        
        const result = originalActions[key].apply(this, args)
        
        console.log(`Action ${key} completed with:`, result)
        
        return result
      }
    }
  })
}

3. State Persistence

js
function createPersistencePlugin(options = {}) {
  return function persistencePlugin({ store, options: storeOptions }) {
    // Check if persistence is enabled for this store
    if (!storeOptions.persist) return
    
    const {
      key = store.$id,
      storage = localStorage,
      serializer = JSON,
      beforeRestore = (state) => state,
      afterRestore = (state) => state
    } = typeof storeOptions.persist === 'object' 
      ? storeOptions.persist 
      : {}
    
    // Restore state from storage
    try {
      const savedState = storage.getItem(key)
      if (savedState) {
        const parsed = serializer.parse(savedState)
        const restored = beforeRestore(parsed)
        store.$patch(restored)
        afterRestore(store.$state)
      }
    } catch (error) {
      console.error('Failed to restore state:', error)
    }
    
    // Save state on changes
    store.$subscribe((mutation, state) => {
      try {
        const serialized = serializer.stringify(state)
        storage.setItem(key, serialized)
      } catch (error) {
        console.error('Failed to persist state:', error)
      }
    })
  }
}

4. Validation Plugin

js
function createValidationPlugin() {
  return function validationPlugin({ store, options }) {
    // Add validation schema if defined
    if (!options.validation) return
    
    const { schema, onError = console.error } = options.validation
    
    // Validate initial state
    validateState(store.$state, schema, onError)
    
    // Validate on state changes
    store.$subscribe((mutation, state) => {
      validateState(state, schema, onError)
    })
    
    return {
      $validate() {
        return validateState(store.$state, schema, onError)
      }
    }
  }
}

function validateState(state, schema, onError) {
  try {
    // Simple validation example
    for (const [key, validator] of Object.entries(schema)) {
      if (!validator(state[key])) {
        throw new Error(`Validation failed for ${key}`)
      }
    }
    return true
  } catch (error) {
    onError(error)
    return false
  }
}

5. Async Actions Plugin

js
function createAsyncActionsPlugin() {
  return function asyncActionsPlugin({ store }) {
    const loadingStates = reactive({})
    const errors = reactive({})
    
    // Wrap async actions
    Object.keys(store).forEach(key => {
      const action = store[key]
      if (typeof action === 'function') {
        store[key] = async function(...args) {
          loadingStates[key] = true
          errors[key] = null
          
          try {
            const result = await action.apply(this, args)
            return result
          } catch (error) {
            errors[key] = error
            throw error
          } finally {
            loadingStates[key] = false
          }
        }
      }
    })
    
    return {
      $loading: readonly(loadingStates),
      $errors: readonly(errors),
      $isLoading: (actionName) => !!loadingStates[actionName],
      $getError: (actionName) => errors[actionName],
      $clearError: (actionName) => {
        errors[actionName] = null
      }
    }
  }
}

Advanced Plugin Techniques

Plugin Composition

js
function composePlugins(...plugins) {
  return function composedPlugin(context) {
    const results = plugins.map(plugin => plugin(context)).filter(Boolean)
    return Object.assign({}, ...results)
  }
}

// Usage
const myPlugin = composePlugins(
  persistencePlugin,
  validationPlugin,
  asyncActionsPlugin
)

pinia.use(myPlugin)

Conditional Plugins

js
function createConditionalPlugin(condition, plugin) {
  return function conditionalPlugin(context) {
    if (condition(context)) {
      return plugin(context)
    }
  }
}

// Only apply to specific stores
const userStorePlugin = createConditionalPlugin(
  ({ store }) => store.$id === 'user',
  userSpecificPlugin
)

// Only apply in development
const devPlugin = createConditionalPlugin(
  () => process.env.NODE_ENV === 'development',
  debugPlugin
)

Plugin with Options

js
function createConfigurablePlugin(defaultOptions = {}) {
  return function configurablePlugin(userOptions = {}) {
    const options = { ...defaultOptions, ...userOptions }
    
    return function plugin({ store }) {
      // Use merged options
      if (options.enableLogging) {
        store.$onAction(({ name, args }) => {
          console.log(`Action ${name} called:`, args)
        })
      }
      
      if (options.enablePersistence) {
        // Add persistence logic
      }
      
      return {
        $options: options
      }
    }
  }
}

// Usage
const myPlugin = createConfigurablePlugin({
  enableLogging: true,
  enablePersistence: false
})

pinia.use(myPlugin({
  enablePersistence: true // Override default
}))

TypeScript Support

Plugin Type Definitions

ts
// types.ts
import { PiniaPluginContext } from 'pinia'

export interface MyPluginOptions {
  enabled?: boolean
  prefix?: string
  storage?: Storage
}

export interface MyPluginProperties {
  $myMethod: () => void
  $myProperty: string
}

export type MyPlugin = (options?: MyPluginOptions) => 
  (context: PiniaPluginContext) => MyPluginProperties

Module Declaration

ts
// Extend Pinia types
declare module 'pinia' {
  export interface PiniaCustomProperties {
    $myMethod: () => void
    $myProperty: string
  }
}

Plugin Implementation

ts
// index.ts
import { PiniaPluginContext } from 'pinia'
import { MyPlugin, MyPluginOptions, MyPluginProperties } from './types'

export const createMyPlugin: MyPlugin = (options = {}) => {
  return ({ store }: PiniaPluginContext): MyPluginProperties => {
    return {
      $myMethod() {
        console.log('Method called')
      },
      $myProperty: options.prefix || 'default'
    }
  }
}

export * from './types'

Testing Plugins

Test Setup

ts
// tests/setup.ts
import { createApp } from 'vue'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach } from 'vitest'

beforeEach(() => {
  const app = createApp({})
  const pinia = createPinia()
  app.use(pinia)
  setActivePinia(pinia)
})

Plugin Tests

ts
// tests/plugin.test.ts
import { describe, it, expect } from 'vitest'
import { defineStore } from 'pinia'
import { createMyPlugin } from '../src'

describe('MyPlugin', () => {
  it('should add properties to store', () => {
    const pinia = createPinia()
    pinia.use(createMyPlugin())
    
    const useTestStore = defineStore('test', {
      state: () => ({ count: 0 })
    })
    
    const store = useTestStore()
    
    expect(store.$myMethod).toBeDefined()
    expect(store.$myProperty).toBe('default')
  })
  
  it('should work with custom options', () => {
    const pinia = createPinia()
    pinia.use(createMyPlugin({ prefix: 'custom' }))
    
    const useTestStore = defineStore('test', {
      state: () => ({ count: 0 })
    })
    
    const store = useTestStore()
    
    expect(store.$myProperty).toBe('custom')
  })
})

Integration Tests

ts
// tests/integration.test.ts
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { createMyPlugin } from '../src'
import TestComponent from './TestComponent.vue'

it('should work in Vue components', () => {
  const pinia = createPinia()
  pinia.use(createMyPlugin())
  
  const wrapper = mount(TestComponent, {
    global: {
      plugins: [pinia]
    }
  })
  
  // Test component behavior with plugin
  expect(wrapper.text()).toContain('Plugin loaded')
})

Publishing Your Plugin

Build Configuration

js
// rollup.config.js
import typescript from '@rollup/plugin-typescript'

export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
      exports: 'named'
    },
    {
      file: 'dist/index.esm.js',
      format: 'es'
    }
  ],
  plugins: [typescript()],
  external: ['pinia', 'vue']
}

Documentation

markdown
# My Pinia Plugin

## Installation

```bash
npm install pinia-my-plugin

Usage

js
import { createPinia } from 'pinia'
import { createMyPlugin } from 'pinia-my-plugin'

const pinia = createPinia()
pinia.use(createMyPlugin({
  // options
}))

API

Options

  • enabled (boolean): Enable/disable plugin
  • prefix (string): Prefix for properties

Added Properties

  • $myMethod(): Custom method
  • $myProperty: Custom property

### Publishing Checklist

- [ ] Tests pass
- [ ] TypeScript definitions included
- [ ] Documentation complete
- [ ] Examples provided
- [ ] Peer dependencies specified
- [ ] Build artifacts generated
- [ ] Version bumped
- [ ] Changelog updated

## Best Practices

### 1. Naming Conventions

```js
// Use $ prefix for plugin properties
return {
  $myMethod() {},     // ✅ Good
  myMethod() {},      // ❌ May conflict with user code
}

2. Error Handling

js
function safePlugin({ store }) {
  try {
    // Plugin logic
    return {
      $safeMethod() {
        try {
          // Method implementation
        } catch (error) {
          console.error('Plugin method failed:', error)
        }
      }
    }
  } catch (error) {
    console.error('Plugin initialization failed:', error)
    return {}
  }
}

3. Performance Considerations

js
function performantPlugin({ store }) {
  // Debounce expensive operations
  const debouncedSave = debounce(() => {
    // Expensive operation
  }, 1000)
  
  store.$subscribe(() => {
    debouncedSave()
  })
  
  // Use WeakMap for store-specific data
  const storeData = new WeakMap()
  storeData.set(store, { /* data */ })
}

4. Memory Management

js
function memoryAwarePlugin({ store }) {
  const cleanup = []
  
  // Set up subscriptions
  const unsubscribe = store.$subscribe(() => {})
  cleanup.push(unsubscribe)
  
  const interval = setInterval(() => {}, 1000)
  cleanup.push(() => clearInterval(interval))
  
  // Clean up when store is disposed
  store.$dispose(() => {
    cleanup.forEach(fn => fn())
  })
}

5. Backward Compatibility

js
function compatiblePlugin({ store, options }) {
  // Check Pinia version
  const piniaVersion = store.$pinia.version || '2.0.0'
  
  if (semver.gte(piniaVersion, '2.1.0')) {
    // Use new features
  } else {
    // Fallback for older versions
  }
}

Common Patterns

Plugin Registry

js
class PluginRegistry {
  constructor() {
    this.plugins = new Map()
  }
  
  register(name, plugin) {
    this.plugins.set(name, plugin)
  }
  
  get(name) {
    return this.plugins.get(name)
  }
  
  createComposed(...names) {
    const plugins = names.map(name => this.get(name))
    return composePlugins(...plugins)
  }
}

const registry = new PluginRegistry()
registry.register('persistence', persistencePlugin)
registry.register('validation', validationPlugin)

const myPlugin = registry.createComposed('persistence', 'validation')

Plugin Middleware

js
function createMiddleware() {
  const middlewares = []
  
  return {
    use(middleware) {
      middlewares.push(middleware)
    },
    
    createPlugin() {
      return function middlewarePlugin(context) {
        let result = {}
        
        for (const middleware of middlewares) {
          const middlewareResult = middleware(context, result)
          if (middlewareResult) {
            result = { ...result, ...middlewareResult }
          }
        }
        
        return result
      }
    }
  }
}

Troubleshooting

Common Issues

  1. Plugin not working: Check if plugin is registered before store creation
  2. TypeScript errors: Ensure module declaration is included
  3. Performance issues: Use debouncing and avoid expensive operations in subscriptions
  4. Memory leaks: Always clean up subscriptions and intervals

Debugging

js
function debugPlugin({ store }) {
  console.log('Plugin applied to store:', store.$id)
  console.log('Store state:', store.$state)
  console.log('Store options:', store.$options)
  
  return {
    $debug: {
      store,
      state: store.$state,
      options: store.$options
    }
  }
}

Released under the MIT License.