Pinia State Management

repository·v4·Indexed 12 days ago

https://github.com/vuejs/pinia

An intuitive, type-safe, and flexible state management store for Vue and the official successor to Vuex. It supports modular store definitions via defineStore, integration with Nuxt through @pinia/nuxt, and advanced patterns for using composables and composing stores in both client-side and SSR environments.

Tokens
31.9K
Snippets
127
Records
147
Agent score
96%

What's inside Pinia

  1. What is Pinia and why use it?

    v4

    Pinia is a state management library for Vue that allows you to share state across components and pages. While you can share state using a simple reactive object in Single Page Applications (SPAs), Pinia provides essential features for production-grade applications, especially those using Server Side Rendering (SSR) to prevent cross-request state pollution.

    Key benefits include:

    • Testing utilities: Built-in support for testing stores.
    • Plugins: Ability to extend Pinia's core features.
    • TypeScript/Autocompletion: Excellent support for TS and autocompletion for JS users.
    • SSR Support: Safe state management for server-side rendered applications.
    • Devtools Support: A timeline to track actions/mutations, visibility of stores in components, and time-travel debugging.
    • Hot Module Replacement (HMR): Modify stores without reloading the page while preserving existing state.
  2. What is a Store?

    v4

    A Store is an entity that hosts global state, holding state and business logic that is not bound to your component tree. It acts like a component that is always present and accessible to all other parts of your application.

    A store consists of three core concepts:

    • State: Equivalent to data in components.
    • Getters: Equivalent to computed properties in components.
    • Actions: Equivalent to methods in components.
  3. Stubbing limitations in Setup stores

    v4

    In Setup stores, stubbing actions has a limitation: if one action calls another action using a closed-over function reference (instead of via the store instance), the stub will be bypassed and the real implementation will run.

    Incorrect (Setup Store):

    export const useCounterStore = defineStore('counter', () => {
      function increment() { /* ... */ }
      function incrementTwice() {
        increment() // ❌ Uses closed-over reference; stub is ignored
      }
      return { increment, incrementTwice }
    })

    Correct (Setup Store): To make internal calls stubbable, route them through the store instance:

    export const useCounterStore = defineStore('counter', () => {
      function increment() { /* ... */ }
      function incrementTwice() {
        const store = useCounterStore()
        store.increment() // ✅ Uses the store instance; stub is used
      }
      return { increment, incrementTwice }
    })

    Note: Options stores do not have this limitation because internal calls use this, which refers to the store instance.

  4. Handle advanced Vuex features in Pinia

    v4

    If your Vuex implementation relies on advanced features, use these Pinia equivalents:

    • Dynamic Modules: Pinia does not require dynamic module registration. Stores are dynamic by design and are only instantiated/registered when they are first used.
    • Hot Module Replacement (HMR): Pinia supports HMR, but the implementation differs from Vuex. Refer to the Pinia HMR guide for setup.
    • Plugins: If you use a Vuex plugin, check for a Pinia alternative first. If you have custom plugins, they can typically be updated to follow the Pinia plugin API.
  5. How Pinia handles modules

    v4
    Unlike Vuex, Pinia does not use a single monolithic tree of dynamic modules. Instead, it encourages a modular approach where you create different stores that can be imported anywhere in your application. This approach ensures type safety, which is often lost with dynamic module patterns.
  6. How Pinia plugins work

    v4

    A Pinia plugin is a function that can augment stores by adding properties, methods, or intercepting actions. Plugins are registered using pinia.use().

    Important Lifecycle Note: Plugins are only applied to stores created after the plugin is installed and after the pinia instance is passed to the Vue app.

    Plugin Context: A plugin receives an optional context object containing:

    • pinia: The Pinia instance created via createPinia().
    • app: The current Vue app instance.
    • store: The specific store being augmented.
    • options: The options object passed to defineStore() for that store.
    export function myPiniaPlugin(context) {
      context.pinia // the pinia instance
      context.app // the current app
      context.store // the store being augmented
      context.options // the options object from defineStore()
    }
  7. Create a Setup Store

    v4

    Setup Stores use a syntax similar to Vue's Composition API. You pass a setup function that defines reactive properties and methods, then returns an object containing what you want to expose.

    • ref() calls become state properties.
    • computed() calls become getters.
    • function() calls become actions.

    Important Requirements:

    • You must return all state properties in the return object. Failing to return state properties or making them readonly will break SSR, devtools, and other plugins.
    • Setup stores allow you to use watchers and other composables inside the store.
    • You can access globally provided properties (like Router or Route) using inject() within a setup store.

    Warning: Do not return non-store properties (like route or injected app-level values) in the return object; access them directly in components instead.

    export const useCounterStore = defineStore('counter', () => {
      const count = ref(0)
      const name = ref('Eduardo')
      const doubleCount = computed(() => count.value * 2)
      function increment() {
        count.value++
      }
    
      return { count, name, doubleCount, increment }
    })
  8. Define business logic with Actions

    v4

    Actions are the Pinia equivalent of component methods and are the primary place to define business logic. They can be synchronous or asynchronous.

    Key characteristics:

    • Access to Store: Actions have access to the entire store instance via this with full type support and autocompletion.
    • Asynchronous Support: Unlike getters, actions can be async and can await API calls or other actions.
    • No Arrow Functions: Because actions rely on this to access the store state and other actions, you cannot use arrow functions for the action definitions themselves.
    • Flexibility: You can define any arguments and return values; Pinia automatically infers types when calling them.
    export const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0 }),
      actions: {
        // Use regular functions to ensure `this` is bound correctly
        increment() {
          this.count++
        },
        async asyncIncrement() {
          const result = await someApiCall()
          this.count += result
        }
      },
    })
  9. Avoid infinite loops when composing stores

    v4

    When two or more stores use each other, you must ensure they do not create an infinite loop through getters or actions. Specifically, stores that depend on each other cannot both directly read each other's state within their setup function.

    To avoid this, access properties of the other store within computed properties or actions instead of at the top level of the setup function.

    const useX = defineStore('x', () => {
      const y = useY()
    
      // ❌ This is not possible because y also tries to read x.name
      y.name
    
      function doSomething() {
        // ✅ Read y properties in computed or actions
        const yName = y.name
        // ...
      }
    
      return {
        name: ref('I am X'),
      }
    })
    
    const useY = defineStore('y', () => {
      const x = useX()
    
      // ❌ This is not possible because x also tries to read y.name
      x.name
    
      function doSomething() {
        // ✅ Read x properties in computed or actions
        const xName = x.name
        // ...
      }
    
      return {
        name: ref('I am Y'),
      }
    })
  10. Auto-imports in @pinia/nuxt

    v4

    The @pinia/nuxt module provides several auto-imports to simplify development:

    • usePinia(): Similar to getActivePinia() but optimized for Nuxt.
    • defineStore(): To define your Pinia stores.
    • storeToRefs(): To extract individual refs from a store.
    • acceptHMRUpdate(): For Hot Module Replacement.

    Additionally, the module automatically imports all stores located in your stores folder (or app/stores in Nuxt 4).

  11. Access other getters using `this`

    v4

    If a getter needs to access other getters within the same store, you must use a regular function instead of an arrow function to gain access to the store instance via this.

    Important for TypeScript users: When using this to access other getters, you must explicitly define the return type of the getter due to TypeScript limitations. If you use an arrow function or do not use this, the return type is typically inferred automatically.

    export const useCounterStore = defineStore('counter', {
      state: () => ({
        count: 0,
      }),
      getters: {
        // automatically infers the return type as a number
        doubleCount(state) {
          return state.count * 2
        },
        // the return type **must** be explicitly set
        doublePlusOne(): number {
          return this.doubleCount + 1
        },
      },
    })