Advanced Pinia Features: Plugins in Nuxt.js

Advanced Pinia Features: Plugins in Nuxt.js

·

4 min read

Pinia plugins allow you to extend the functionality of your stores by adding reusable features like persisted state, authentication handling, or logging.

1. What are Pinia Plugins?

A Pinia plugin is a function that runs when a store is initialized. It allows you to:
✅ Extend state with extra properties
✅ Add global actions or getters
✅ Automatically persist data (e.g., in localStorage)
✅ Handle authentication, logging, or analytics

2. How to Create a Custom Pinia Plugin in Nuxt

You can create your own Pinia plugin to extend store functionality globally.

Step 1: Create a Plugin (plugins/piniaPlugin.ts)

import { PiniaPluginContext } from 'pinia'

export default function myPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation, state) => {
    console.log(`[Pinia]: Mutation in ${store.$id}:`, mutation, state)
  })

  return {
    version: '1.0.0', // Adding a custom global property
  }
}

This plugin logs all store mutations and adds a version property to every store.


Step 2: Register the Plugin in nuxt.config.ts

Modify your nuxt.config.ts to enable the plugin:

export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  pinia: {
    autoImports: ['defineStore'],
  },
  plugins: ['~/plugins/piniaPlugin.ts'], // Register the Pinia plugin
})

Step 3: Use the Plugin in a Store

Now, every store can access the version property:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

Access the plugin property in a component:

<script setup lang="ts">
import { useCounterStore } from '~/stores/counter'

const counter = useCounterStore()

console.log(counter.version) // Output: '1.0.0'
</script>

3. Built-in Pinia Plugins (Example: Persisted State)

One of the most popular built-in plugins is pinia-plugin-persistedstate, which allows automatic localStorage or sessionStorage persistence.

Step 1: Install the Plugin

npm install pinia-plugin-persistedstate

Step 2: Register in plugins/piniaPersist.ts

import { defineNuxtPlugin } from '#app'
import { Pinia } from 'pinia'
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'

export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(piniaPluginPersistedState)
})

Step 3: Enable Persistence in a Store

import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: ''
  }),
  persist: true // This automatically stores the state in localStorage
})

Now, token is persisted across page reloads.


4. When Should You Use Pinia Plugins?

Global Enhancements – Logging, analytics, authentication, or caching
Auto Persistence – Save user preferences, authentication tokens
Cross-Store Features – Shared behavior across multiple stores

II. Authentication with Pinia in Nuxt.js

Managing authentication in Nuxt.js with Pinia is a clean and efficient approach. We'll implement:
User login/logout
Storing tokens securely
Auto-persisting authentication state

1. Setting Up an Authentication Store
First, create an authentication store to handle user sessions.
Updated Auth Store (Improved Security & Maintainability)

stores/auth.ts

import { defineStore } from 'pinia'
import { useCookie } from '#app'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as null | { id: number; name: string; email: string },
    token: useCookie('auth_token'), // Securely store token in a cookie
    isAuthenticated: false,
    loading: false,
    error: null as string | null,
  }),

  getters: {
    getUser: (state) => state.user,
    getToken: (state) => state.token.value, // Use cookie value
    isUserAuthenticated: (state) => state.isAuthenticated,
    getError: (state) => state.error,
  },

  actions: {
    async login(credentials: { email: string; password: string }) {
      try {
        this.loading = true
        this.error = null

        const response = await $fetch('/api/auth/login', {
          method: 'POST',
          body: credentials,
        })

        if (!response?.token) throw new Error('Invalid response from server')

        // Store token securely
        this.token.value = response.token

        this.user = response.user
        this.isAuthenticated = true

        navigateTo('/dashboard') // Redirect user after login
      } catch (error) {
        this.error = error.message || 'Login failed'
        throw error
      } finally {
        this.loading = false
      }
    },

    async logout() {
      try {
        await $fetch('/api/auth/logout', {
          method: 'POST',
        })
      } catch (error) {
        console.error('Logout error:', error)
      } finally {
        // Clear user session securely
        this.token.value = null
        this.user = null
        this.isAuthenticated = false

        navigateTo('/login') // Redirect to login
      }
    },

    async checkAuth() {
      if (!this.token.value) {
        this.logout()
        return
      }

      try {
        const response = await $fetch('/api/auth/verify', {
          headers: {
            Authorization: `Bearer ${this.token.value}`,
          },
        })

        this.user = response.user
        this.isAuthenticated = true
      } catch (error) {
        this.logout()
      }
    },

    async register(userData: { name: string; email: string; password: string }) {
      try {
        this.loading = true
        this.error = null

        await $fetch('/api/auth/register', {
          method: 'POST',
          body: userData,
        })

        // Auto-login after successful registration
        await this.login({
          email: userData.email,
          password: userData.password,
        })
      } catch (error) {
        this.error = error.message || 'Registration failed'
        throw error
      } finally {
        this.loading = false
      }
    },
  },

  persist: {
    enabled: true, // Automatically persists the auth state
  },
})