Pinia Internals

Preview — 3 of 10 questions

How does Pinia maintain a single store instance across multiple useMyStore() calls?

javascript
// Simplified Pinia internals
class Pinia {
  _s = new Map<string, Store>() // Store registry: id → store instance
  state = ref<Record<string, StateTree>>({}) // Global state tree
}

function defineStore(id, setup) {
  function useStore() {
    const pinia = getActivePinia() // Current Pinia instance (set by app.use(pinia))

    // Return cached instance if exists
    if (pinia._s.has(id)) {
      return pinia._s.get(id)!
    }

    // Create new store instance (only once)
    let scope: EffectScope
    const store = markRaw({
      $id: id,
      $patch: (patch) => { ... },
      $subscribe: (callback) => { ... },
      $onAction: (callback) => { ... },
      $dispose: () => scope.stop()
    })

    // Run the setup function in a detached effectScope
    scope = effectScope(true) // true = detached from component scope
    scope.run(() => {
      const setupResult = setup()
      // Merge setup result onto store object
      Object.assign(store, setupResult)
    })

    pinia._s.set(id, store) // Cache it
    return store
  }

  return useStore
}
APinia uses a JavaScript singleton pattern with a global module-level variable
BEach useMyStore() call creates a new instance — Pinia merges them automatically
CPinia uses Symbol keys in WeakMap to associate store instances with components
DPinia stores the store instance in the active Pinia instance keyed by store ID, returning the cached instance on subsequent calls

How do you write a Pinia plugin that creates watchers and cleans them up when the store is disposed?

javascript
import type { PiniaPluginContext } from 'pinia'
import { watch, effectScope } from 'vue'

function syncToLocalStoragePlugin({ store, options }: PiniaPluginContext) {
  const syncConfig = options.sync // Custom option from defineStore

  if (!syncConfig) return

  const key = `pinia:${store.$id}`

  // Rehydrate from storage
  const saved = localStorage.getItem(key)
  if (saved) {
    try {
      store.$patch(JSON.parse(saved))
    } catch { /* ignore invalid data */ }
  }

  // Create watcher inside the store's effectScope
  // store._e is the internal effectScope — effects here are disposed with store.$dispose()
  store._e.run(() => {
    watch(
      () => JSON.stringify(store.$state),
      (serialized) => {
        localStorage.setItem(key, serialized)
      },
      { deep: true, flush: 'post' }
    )
  })
}

// Usage
export const useSettingsStore = defineStore('settings', {
  state: () => ({ theme: 'light', language: 'en' }),
  sync: true // Custom option consumed by plugin
})

// TypeScript: extend defineStore options
declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    sync?: boolean
  }
}
AUse onUnmounted inside the plugin — Pinia hooks into Vue's lifecycle
BUse the store.$dispose hook — plugins cannot create long-lived effects
CCreate effects inside the plugin using effectScope captured from the store's internal scope via store._e
DUse watchEffect in the plugin and return a cleanup function

What are the security implications of transferring Pinia state from server to client?

javascript
// ❌ VULNERABLE: naive JSON embedding
const html = `
  <script>window.__PINIA_STATE__ = ${JSON.stringify(pinia.state.value)}</script>
`
// If any state contains </script>, it breaks out of the script tag:
// state: { name: '</script><script>alert("XSS")</script>' }
// Results in: window.__PINIA_STATE__ = { name: "</script><script>alert("XSS")</script>" }

// ✅ SECURE: use devalue (Nuxt's approach) or escape </script>
import { devalue } from '@nuxt/devalue' // Handles Date, RegExp, circular refs, and XSS

const stateScript = `<script>window.__PINIA_STATE__ = ${devalue(pinia.state.value)}</script>`

// ✅ Alternative: manual escaping
function serializeState(state: unknown): string {
  return JSON.stringify(state)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
}

// ✅ SECURITY: never transfer sensitive state to the client
// Filter out sensitive data before serialization:
const transferableState = {
  ...pinia.state.value,
  auth: {
    // Don't transfer: tokens, full user objects with PII
    isAuthenticated: !!pinia.state.value.auth.user,
    userId: pinia.state.value.auth.user?.id // Only what the client needs
  }
}
AState containing sensitive data (tokens, PII) can be exposed via XSS; improperly escaped JSON can enable script injection attacks
BThere are no security implications — the data is already public
CSSR state transfer is automatically secured by Vue's template escaping
DThe only risk is performance — security is not a concern for state transfer

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.