Official website through train

What is Pinia?

According to the website, Pinia was originally an experiment in redesigning the Vue Store using the Composition API around November 2019. From then on, the original principles are still the same, but Pinia works with Vue 2 and Vue 3 and doesn’t require you to use the composite API. The API for both is the same except for installation and SSR, and the documentation is specific to Vue 3 and provides comments about Vue 2 where necessary so that Vue 2 and Vue 3 users can read it!

Pinia is a lightweight state management library for vue.js that has become very popular recently. It uses the new reaction system in Vue 3 to build an intuitive and fully typed state management library.

Pinia’s success can be attributed to its unique ability to manage stored data (scalability, storage module organization, grouping of state changes, creation of multiple stores, and so on).

Vuex, on the other hand, is a popular state management library built for the Vue framework and recommended by the Vue core team. Vuex has a strong focus on application scalability, developer productivity and confidence. It is based on the same traffic architecture as Redux.

I’ve been paying attention to Pinia since Judas mentioned Pinia on the Nuggets’ No.1 night talk in March. Utah made it clear: “There will be no Vex5, or pinia will be a Vex5. Pinia is a library that vuex maintainers made when deciding on the style of Vue 5. It worked better than expected and will not be renamed to Vue 5 in respect of the authors. Pinia is a repository for Vue that allows you to share state across components/pages. If you’re familiar with the Composition API, you might think you can already use the simple export const state = Reactive ({}). This is true for single-page applications, but if it is rendered server-side, it exposes your application to security vulnerabilities. But even in small, single-page applications, you can get a lot of benefits from using Pinia:

  • Development tool support

    • A timeline to track actions, mutations
    • Stores appear in components where they are used
    • Time travel and easier debugging
  • Replacing a hot module

    • Modify your Store without reloading the page
    • Maintain any existing state at development time
  • Plug-ins: Use plug-ins to extend Pinia functionality

  • Provide appropriate TypeScript support or auto-complete functionality for JS users

  • Server-side rendering support

Basic example

This is what pinia looks like in terms of the API (be sure to check the official website for a full introduction). You first create a Store:

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // could also be defined as
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})
Copy the code

Then use it in the component:

import { useCounterStore } from '@/stores/counter' export default { setup() { const counter = useCounterStore() Counter. Counter ++ // with autocompletion ✨ counter.$patch({count: counter.count + 1 }) // or using an action instead counter.increment() }, }Copy the code

You can even use a function (similar to a component setup()) to define a Store for more advanced use cases:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})
Copy the code

If you’re still unfamiliar with the Setup ()Composition API, don’t worry; Pinia also supports a similar set of Map helpers, such as Vuex. You define storage in the same way, but then use mapStores(), mapState(), or mapActions() :

const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { double: (state) => state.count * 2, }, actions: { increment() { this.count++ } } }) const useUserStore = defineStore('user', { // ... }) export default { computed: { // other computed properties // ... // gives access to this.counterStore and this.userStore ... mapStores(useCounterStore, useUserStore) // gives read access to this.count and this.double ... mapState(useCounterStore, ['count', 'double']), }, methods: { // gives access to this.increment() ... mapActions(useCounterStore, ['increment']), }, }Copy the code

You’ll find more information about each map helper in the core concepts.

Why choosePinia#

Pinia (pronounced/PI ːnjʌ/, as in English “peenya”) is the closest word to pina (Spanish for pineapple), which is a valid package name. A pineapple is actually a group of individual flowers that come together to form multiple fruits. Like the Store, each was born independently, but ultimately connected. It is also a delicious tropical fruit, native to South America.

A more realistic example#

This is a more complete EXAMPLE of an API that you will use in Pinia, even in JavaScript. For some, this may be enough to get started without further reading, but we still recommend checking the rest of the document, even skipping this example, and returning after reading all the core concepts.

import { defineStore } from 'pinia' export const todos = defineStore('todos', { state: () => ({ /** @type {{ text: string, id: number, isFinished: boolean }[]} */ todos: [], /** @type {'all' | 'finished' | 'unfinished'} */ filter: 'all', // type will be automatically inferred to number nextId: 0, }), getters: { finishedTodos(state) { // autocompletion! ✨ return state.todos.filter((todo) => todo.isfinished)}, unfinishedTodos(state) {return state.todos.filter((todo) =>! todo.isFinished) }, /** * @returns {{ text: string, id: number, isFinished: Boolean}[]} */ filteredTodos(state) {if (this.filter === 'finished') {// Call other getters with autocompletion ✨ return this.finishedTodos } else if (this.filter === 'unfinished') { return this.unfinishedTodos } return this.todos }, }, actions: { // any amount of arguments, return a promise or not addTodo(text) { // you can directly mutate the state this.todos.push({ text, id: this.nextId++, isFinished: false }) }, }, })Copy the code

Comparison with Vuex#

Pinia was originally intended to explore what the next iteration of Vuex would look like, combining many of the ideas discussed by the Vuex 5 core team. Eventually, we realized that Pinia already implemented most of what we wanted in Vuex 5, and decided to implement it instead with a new proposal.

Compared to Vuex, Pinia offers a simpler API with fewer rituals, a composition-API style API, and, most importantly, reliable type inference support when used with TypeScript.

RFC#

Initially Pinia did not pass any RFC. I tested ideas based on my experience developing applications, reading other people’s code, working for clients using Pinia, and answering questions on Discord. This allows me to provide a solution that works for a variety of situations and application sizes. I used to publish and keep the library growing while keeping its core API intact.

Pinia is now the default state management solution, following the SAME RFC process as the other core libraries in the Vue ecosystem, and its API has entered a stable state.

Comparison with Vuex 3.x/4.x#

Vuex 3.x corresponds to Vue 2 while Vuex 4.x corresponds to Vue 3

The Pinia API is very different from Vuex ≤4, namely:

  • mutationNo longer exists. They are often thought to beverylong. They initially brought DevTools integration, but that’s no longer an issue.
  • There is no need to create complex custom wrappers to support TypeScript, everything is typed, and the API is designed to take advantage of TS type inference wherever possible.
  • No more injecting magic strings, importing functions, calling them, and enjoying auto-complete!
  • There is no need to add stores dynamically; by default they are all dynamic and you won’t even notice. Note that you can still register manually with Store at any time, but because it’s automatic, you don’t have to worry.
  • There is no longerThe moduleThe nested structure of. You can still implicitly nest stores by importing and using stores in another Store, but Pinia provides a flat structure by design while still supporting cross-store composition.You can even have cyclic store dependencies.
  • There is noModule of the namespace. Given the flat architecture of stores, “namespace” stores are inherent in the way they are defined, and you could say that all stores are namespace.

For more detailed instructions on how to convert an existing Vuex ≤4 project to Pinia, please refer to the migration guide from Vuex on the official website.

The installation#

Pinia is installed using your favorite package manager:

yarn add pinia
# or with npm
npm install pinia
# or with pnpm
pnpm install pinia
Copy the code

prompt

If your application uses Vue 2, you will also need to install the composition API: @vue/ composition-API. If you use Nuxt, you should follow these instructions.

If you are using the Vue CLI, you can try this unofficial plugin.

Create a pinia (root storage) and pass it to the application:

import { createPinia } from 'pinia'

app.use(createPinia())
Copy the code

If you are using Vue 2, you will also need to install a plug-in and Pinia will inject the created plug-in into the root directory of the application:

import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
  el: '#app',
  // other options...
  // ...
  // note the same `pinia` instance can be used across multiple Vue apps on
  // the same page
  pinia,
})
Copy the code

This will also add DevTools support. In Vue 3, some features such as time travel and editing are still not supported because vue-DevTools does not yet expose the necessary apis, but DevTools has more features and the overall developer experience is much better. In Vue 2, Pinia uses Vuex’s existing interface (and therefore cannot be used with it).

What is a Store?#

A Store, such as Pinia, is an entity that holds state and business logic that is not bound to your component tree. In other words, it hosts global state. It’s kind of like a component that’s always there and that everyone can read and write to. It contains three concepts, state, getter, and Action, and it is safe to assume that these concepts are equivalent to Data,computed, and Methods in the component.

When should I use Store#

The store should contain data that can be accessed throughout the application. This includes data that is used in many places, such as the user information displayed in the navigation bar, and data that needs to be saved through a page, such as a very complex multi-step form.

On the other hand, you should avoid including local data in the store that might be hosted in the component, such as visibility of local elements of the page.

Not all applications need access to global state, but if you need one, Pania will make your life easier.

Define the Store

Before delving into the core concepts, we need to know that the store uses defineStore() and that it needs a unique name, passed as the first argument:

import { defineStore } from 'pinia'

// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const useStore = defineStore('main', {
  // other options...
})
Copy the code

This name, also known as id, is required, and Pania uses it to connect the Store to DevTools. Name the returned function use… Is a convention between combinable items to make them habitual.

The use of the Store#

We are defining useStore() as a Store, because Store will not be created setup() before it is called:

import { useStore } from '@/stores/counter'

export default {
  setup() {
    const store = useStore()

    return {
      // you can return the whole store instance to use it in the template
      store,
    }
  },
}
Copy the code

You can define as many stores as you need, and each Store should be defined in a different file to take full advantage of Pinia (for example, automatically allowing code splitting and TypeScript reasoning for your packages).

If you are not already using the Setup component, you can still use Pinia with Map Helpers.

After you instantiate a Store, you can access any properties defined in State, getters, and directly in the Store. Actions We’ll cover these in more detail in the next page, but autocomplete sessions can help.

Note that the store is a wrapped object reactive, which means that the value is not written after the getter, but like the propsin setup, we can’t deconstruct it:

Export default defineComponent({setup() {const store = useStore() // ❌ This won't work because it breaks reactivity //  it's the same as destructuring from `props` const { name, doubleCount } = store name // "eduardo" doubleCount // 2 return { // will always be "eduardo" name, // will always be 2 doubleCount, // this one will be reactive doubleValue: computed(() => store.doubleCount), } }, })Copy the code

To extract properties from storage while keeping them reactive, you need to use storeToRefs(). It creates a reference for each of the reaction properties. This is useful when you just use the state in the Store without invoking any operations. Note that you can deconstruct operations directly from the Store, since they are also bound to the Store itself:

import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    const store = useStore()
    // `name` and `doubleCount` are reactive refs
    // This will also create refs for properties added by plugins
    // but skip any action or non reactive (non ref/reactive) property
    const { name, doubleCount } = storeToRefs(store)
    // the increment action can be just extracted
    const { increment } = store

    return {
      name,
      doubleCount
      increment,
    }
  },
})
Copy the code

State of the State

Most of the time, the state is the central part of the Store. People usually start by defining the state of the application that represents them. In Pinia, a state is defined as a function that returns an initial state. This allows Pinia to work both server-side and client-side.

import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // arrow function recommended for full type inference
  state: () => {
    return {
      // all these properties will have their type inferred automatically
      counter: 0,
      name: 'Eduardo',
      isAdmin: true,
    }
  },
})
Copy the code

prompt

If you use Vue 2, the data state you create in it follows the same rule data as in the Vue instance, that is, the state object must be normal and you need to call it when you add a new attribute vue.set () to it. See also: Vue#data. * * * *

accessstate#

By default, you can read and write state directly via store instance access state:

const store = useStore()

store.counter++
Copy the code

Reset the state#

You can reset the state to its original value by calling a method on the store: $reset()

const store = useStore()

store.$reset()
Copy the code

Using the Options API

For the following example, you can assume that the following stores have been created:

// Example File Path:
// ./src/stores/counterStore.js

import { defineStore } from 'pinia',

const useCounterStore = defineStore('counterStore', {
  state: () => ({
    counter: 0
  })
})
Copy the code

andsetup()#

While the Composition API isn’t for everyone, the Setup () hook makes it easier to use Pinia in the Options API. No additional Map Helper functionality is required!

import { useCounterStore } from '.. /stores/counterStore' export default { setup() { const counterStore = useCounterStore() return { counterStore } }, computed: { tripleCounter() { return this.counterStore.counter * 3 }, }, }Copy the code

There is nosetup()#

If you are not using the Composition API and you are using computed, methods,… , you can use the mapState() helper to map the State property to a read-only computed property:

import { mapState } from 'pinia' import { useCounterStore } from '.. /stores/counterStore' export default { computed: { // gives access to this.counter inside the component // same as reading from store.counter ... mapState(useCounterStore, ['counter']) // same as above but registers it as this.myOwnName ... mapState(useCounterStore, { myOwnName: 'counter', // you can also write a function that gets access to the store double: store => store.counter * 2, // it can have access to `this` but it won't be typed correctly... magicValue(store) { return store.someGetter + this.counter + this.double }, }), }, }Copy the code

Can modify the State#

If you want to be able to write these #### State properties (for example, if you have a form), you can use mapWritableState() instead. Note that you cannot pass a function like mapState() with:

import { mapWritableState } from 'pinia' import { useCounterStore } from '.. /stores/counterStore' export default { computed: { // gives access to this.counter inside the component and allows setting it // this.counter++ // same as reading from store.counter ... mapWritableState(useCounterStore, ['counter']) // same as above but registers it as this.myOwnName ... mapWritableState(useCounterStore, { myOwnName: 'counter', }), }, }Copy the code

prompt

You don’t need mapWritableState() for collections like arrays, unless you replace the entire array with cartItems = [],mapState() still allows you to call methods on collections.

Change state#

In addition to changing store directly, you can also call the $patch method, store.counter++. State allows you to apply multiple changes simultaneously to parts of an object:

store.$patch({
  counter: store.counter + 1,
  name: 'Abalam',
})
Copy the code

However, some mutations can be difficult or expensive to apply using this syntax: any collection modification (for example, pushing, deleting, concatenating elements from an array) requires you to create a new collection. Because of this, the $patch method also accepts a function to group mutations that are difficult to apply to patch objects:

cartStore.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})
Copy the code

The main difference here is that $patch() allows you to group multiple changes into a single entry in DevTools. Note that the state is changed directly and $patch() appears in DevTools and can travel through time (not yet in Vue 3).

replacestate#

$state You can replace the entire state of the Store by setting the Store property to the new object:

store.$state = { counter: 666, name: 'Paimon' }
Copy the code

You can also replace the entire state of the application by changing the instance state of. Pinia This is used for hydration during SSR.

pinia.state.value = {}
Copy the code

Subscribe to the state#

You can observe the state and its changes by using the Store method $SUBSCRIBE (), similar to Vuex’s Subscribe method. The advantage of using $subscribe() over regular watch() is that the subscription is triggered only once after the patch (for example, when using the above version of the function).

cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // same as cartStore.$id
  mutation.storeId // 'cart'
  // only available with mutation.type === 'patch object'
  mutation.payload // patch object passed to cartStore.$patch()

  // persist the whole state to the local storage whenever it changes
  localStorage.setItem('cart', JSON.stringify(state))
})
Copy the code

By default, status subscriptions are bound to the component to which they are added (if stored inside the component setup()). This means that when components are uninstalled, they are automatically removed. Detached: true if you want to keep components after they are detached, pass {detached: true} as a second argument to separate the state subscription from the current component:

export default { setup() { const someStore = useSomeStore() // this subscription will be kept after the component is unmounted someStore.$subscribe(callback, { detached: true }) // ... }},Copy the code

prompt

Pinia You can view the entire state on the instance:

watch(
  pinia.state,
  (state) => {
    // persist the whole state to the local storage whenever it changes
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)
Copy the code

Getters

The Getter is exactly the same as the computed value of the Store state. They can define defineStore() with the getters property in. They accept state as the first argument to encourage the use of arrow functions:

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
  },
})
Copy the code

Most of the time, getters depend only on state, but they may need to use other getters. Therefore, we can access the entire Store instance while defining regular functions, but we need to define the type of the return type (in TypeScript). This is due to a known limitation in TypeScript that does not affect getters defined using arrow functions, nor getters not used: this**** this

export const useStore = defineStore('main', { state: () => ({ counter: 0, }), getters: { // automatically infers the return type as a number doubleCount(state) { return state.counter * 2 }, // the return type **must** be explicitly set doublePlusOne(): Number {// Autocompletion and typings for the whole store ✨ return this.doubleCount + 1},},})Copy the code

You can then access the getter directly on the store instance:

<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>

<script>
export default {
  setup() {
    const store = useStore()

    return { store }
  },
}
</script>
Copy the code

Accessing other getters#

As with evaluating properties, you can combine multiple getters. By accessing any other Gettersthis. Even if you don’t use TypeScript, you can use JSDoc to indicate your IDE type:

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // type is automatically inferred because we are not using `this`
    doubleCount: (state) => state.counter * 2,
    // here we need to add the type ourselves (using JSDoc in JS). We can also
    // use this to document the getter
    /**
     * Returns the counter value times two plus one.
     *
     * @returns {number}
     */
    doubleCountPlusOne() {
      // autocompletion ✨
      return this.doubleCount + 1
    },
  },
})
Copy the code

Pass the parameter to the getter#

Getters are just properties that are evaluated behind the scenes, so it is impossible to pass any parameters to them. However, you can return a function from the getter to take any argument:

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})
Copy the code

And used in components:

<script>
export default {
  setup() {
    const store = useStore()

    return { getUserById: store.getUserById }
  },
}
</script>

<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>
Copy the code

Note that when you do this, the getters are no longer cached; they are just the functions you call. However, you can cache some results inside the getter itself, which is not common, but should prove to be better performance:

export const useStore = defineStore('main', {
  getters: {
    getActiveUserById(state) {
      const activeUsers = state.users.filter((user) => user.active)
      return (userId) => activeUsers.find((user) => user.id === userId)
    },
  },
})
Copy the code

Access Getters from other stores#

To use other storage getters, you can use it directly inside the getter: **

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})
Copy the code

Usage andsetup()#

You can directly access any getter as a store property (exactly the same as the state property) :

export default {
  setup() {
    const store = useStore()

    store.counter = 3
    store.doubleCount // 6
  },
}
Copy the code

Using the Options API

For the following example, you can assume that the following stores have been created:

// Example File Path:
// ./src/stores/counterStore.js

import { defineStore } from 'pinia',

const useCounterStore = defineStore('counterStore', {
  state: () => ({
    counter: 0
  }),
  getters: {
    doubleCounter() {
      return this.counter * 2
    }
  }
})
Copy the code

andsetup()#

While the Composition API isn’t for everyone, the Setup () hook makes it easier to use Pinia in the Options API. No additional Map Helper functionality is required!

import { useCounterStore } from '.. /stores/counterStore' export default { setup() { const counterStore = useCounterStore() return { counterStore } }, computed: { quadrupleCounter() { return counterStore.doubleCounter * 2 }, }, }Copy the code

There is nosetup()#

You can map to the getter using the same mapState() function you used in the previous section of state:

import { mapState } from 'pinia' import { useCounterStore } from '.. /stores/counterStore' export default { computed: { // gives access to this.doubleCounter inside the component // same as reading from store.doubleCounter ... mapState(useCounterStore, ['doubleCount']) // same as above but registers it as this.myOwnName ... mapState(useCounterStore, { myOwnName: 'doubleCounter', // you can also write a function that gets access to the store double: store => store.doubleCount, }), }, }Copy the code

Actions

Actions correspond to methods in a component. They can be defined using the Actionsin attribute, defineStore() and are great for defining business logic:

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  actions: {
    increment() {
      this.counter++
    },
    randomizeCounter() {
      this.counter = Math.round(100 * Math.random())
    },
  },
})
Copy the code

Like getters, Actions support access to the entire Store instance through full input (and auto-completion ✨). Unlike them, they can be asynchronous, and you can make any API call or even other operations inside them! This is an example using Mande. Note that it doesn’t matter which library you use, as long as you get an A, you can even use native functions (browser only) : this**** actionsawaitPromise ‘ ‘fetch

import { mande } from 'mande' const api = mande('/api/users') export const useUsers = defineStore('users', { state: () => ({ userData: null, // ... }), actions: { async registerUser(login, password) { try { this.userData = await api.post({ login, password }) showTooltip(`Welcome back ${this.userData.name}! `) } catch (error) { showTooltip(error) // let the form component display the error return error } }, }, })Copy the code

You are also completely free to set any parameters you want and return anything you want. When an action is called, everything is automatically inferred!

Action is called like a method:

export default defineComponent({
  setup() {
    const main = useMainStore()
    // call the action as a method of the store
    main.randomizeCounter()

    return {}
  },
})
Copy the code

Access other Store operations#

To use another Store, you can use it directly within the action: **

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})
Copy the code

Usage andsetup()#

You can call any operation directly as a store method:

export default {
  setup() {
    const store = useStore()

    store.randomizeCounter()
  },
}
Copy the code

Using the Options API

For the following example, you can assume that the following stores have been created:

// Example File Path:
// ./src/stores/counterStore.js

import { defineStore } from 'pinia',

const useCounterStore = defineStore('counterStore', {
  state: () => ({
    counter: 0
  }),
  actions: {
    increment() {
      this.counter++
    }
  }
})
Copy the code

andsetup()#

While the Composition API isn’t for everyone, the Setup () hook makes it easier to use Pinia in the Options API. No additional Map Helper functionality is required!

import { useCounterStore } from '.. /stores/counterStore' export default { setup() { const counterStore = useCounterStore() return { counterStore } }, methods: { incrementAndPrint() { this.counterStore.increment() console.log('New Count:', this.counterStore.count) }, }, }Copy the code

There is nosetup()#

If you don’t want to use the Composition API at all, you can use the mapActions() helper to map action properties to methods in the component:

import { mapActions } from 'pinia' import { useCounterStore } from '.. /stores/counterStore' export default { methods: { // gives access to this.increment() inside the component // same as calling from store.increment() ... mapActions(useCounterStore, ['increment']) // same as above but registers it as this.myOwnName() ... mapActions(useCounterStore, { myOwnName: 'doubleCounter' }), }, }Copy the code

Subscribe to the operation#

You can observe the Action and its result store.$onAction(). The callback passed to it is executed before the operation itself. After handles commitments and allows you to perform functions after the action is resolved. In a similar way, onError allows you to execute functions when operating on throw or reject. These are useful for tracking errors at run time, similar to this technique in the Vue documentation.

This is an example of logging operations before they are run and after they are resolved/rejected.

const unsubscribe = someStore.$onAction(
  ({
    name, // name of the action
    store, // store instance, same as `someStore`
    args, // array of parameters passed to the action
    after, // hook after the action returns or resolves
    onError, // hook if the action throws or rejects
  }) => {
    // a shared variable for this specific action call
    const startTime = Date.now()
    // this will trigger before an action on `store` is executed
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // this will trigger if the action succeeds and after it has fully run.
    // it waits for any returned promised
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // this will trigger if the action throws or returns a promise that rejects
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// manually remove the listener
unsubscribe()
Copy the code

By default, action subscriptions are bound to the component to which they were added (if the Store is inside the component setup()). This means that when components are uninstalled, they are automatically removed. If you want to keep components after uninstalling them, pass true as the second argument to separate the action subscription from the current component: **

export default { setup() { const someStore = useSomeStore() // this subscription will be kept after the component is unmounted someStore.$onAction(callback, true) // ... }},Copy the code

The Plugins plugin

The Pania Store can be fully extended due to low-level apis. Here is a list of actions you can perform:

  • Add new properties to Store
  • Add a new option when defining a Store
  • Add a new method to Store
  • Existing methods of packaging
  • Change or even cancel operations
  • Implement side effects like local storage
  • Only applicable to specific stores

The plug-in is added to the pinia instance pinia.use(). The simplest example is to add a static property to all stores by returning an object:

import { createPinia } from 'pinia' // add a property named `secret` to every store that is created after this plugin is  installed // this could be in a different file function SecretPiniaPlugin() { return { secret: 'the cake is a lie' } } const pinia = createPinia() // give the plugin to pinia pinia.use(SecretPiniaPlugin) // in another file const store = useStore() store.secret // 'the cake is a lie'Copy the code

This is useful for adding global objects such as routers, schemas, or toast managers.

introduce#

The Pinia plug-in is a function that optionally returns properties to add to the Store. It takes an optional argument, a context:

export function myPiniaPlugin(context) {
  context.pinia // the pinia created with `createPinia()`
  context.app // the current app created with `createApp()` (Vue 3 only)
  context.store // the store the plugin is augmenting
  context.options // the options object defining the store passed to `defineStore()`
  // ...
}
Copy the code

Then pass this function to piniawith pinia.use() :

pinia.use(myPiniaPlugin)
Copy the code

Plugins only apply to stores that create pinia** after being passed to the app, otherwise they will not be applied.

Expansion of the Store#

You can add attributes to each Store by simply returning their objects in the plug-in:

pinia.use(() => ({ hello: 'world' }))
Copy the code

You can also set the properties directly on, store, but if possible, use the returned version so that they can be tracked automatically by DevTools:

pinia.use(({ store }) => {
  store.hello = 'world'
})
Copy the code

Any properties returned by the plug-in will be automatically tracked by DevTools, so in order for Hello to be visible in DevTools, make sure to add it to the Store._customProperties dev mode only if you want to debug it in DevTools:

// from the example above
pinia.use(({ store }) => {
  store.hello = 'world'
  // make sure your bundler handle this. webpack and vite should do it by default
  if (process.env.NODE_ENV === 'development') {
    // add any keys you set on the store
    store._customProperties.add('hello')
  }
})
Copy the code

Note that each Store uses wrapped reactive, and automatically unwraps any Ref it contains (Ref (), computed(),…) :

const sharedRef = ref('shared') pinia.use(({ store }) => { // each store has its individual `hello` property store.hello  = ref('secret') // it gets automatically unwrapped store.hello // 'secret' // all stores are sharing the value `shared`  property store.shared = sharedRef store.shared // 'shared' })Copy the code

This is why you can access all the calculated attributes.value without them and why they are reactive.

Adding a New state#

If you want to add a new status attribute to the Store or an attribute you intend to use during hydration, you must add it in two places:

  • instoreSo you can access itstore.myState
  • store.$stateSo it can be used in DevTools and inSSR is serialized during.

Note that this allows you to share reforcomputed attributes:

const globalSecret = ref('secret') pinia.use(({ store }) => { // `secret` is shared among all stores store.$state.secret  = globalSecret store.secret = globalSecret // it gets automatically unwrapped store.secret // 'secret' const hasError =  ref(false) store.$state.hasError = hasError // this one must always be set store.hasError = toRef(store.$state, 'hasError') // in this case it's better not to return `hasError` since it // will be displayed in the `state` section in  the devtools // anyway and if we return it, devtools will display it twice. })Copy the code

Note that state changes or additions that occur in the plug-in (including calls to store.$patch()) occur before the store is active, so no subscriptions are triggered.

warning

If you are using Vue 2, Pinia gets the same reactivity warning as Vue. Use setFrom to create new state properties, such as and: @vue/composition-api ‘ ‘secret’ ‘hasError

import { set } from '@vue/composition-api' pinia.use(({ store }) => { if (! store.$state.hasOwnProperty('hello')) { const secretRef = ref('secret') // If the data is meant to be used during SSR, you should // set it on the `$state` property so it is serialized and // picked up during hydration set(store.$state, 'secret', secretRef) // set it directly on the store too so you can access it // both ways: `store.$state.secret` / `store.secret` set(store, 'secret', secretRef) store.secret // 'secret' } })Copy the code

Add new external attributes#

When adding external attributes, class instances from other libraries, or just something non-reactive, you should wrap the object markRaw() before passing it to Pinia. Here is an example of adding a router to each Store:

import { markRaw } from 'vue' // adapt this based on where your router is import { router } from './router' pinia.use(({  store }) => { store.router = markRaw(router) })Copy the code

$subscribeCalled inside the plug-in#

You can also use in plug-in store. The subscribe] (https://pinia.vuejs.org/core-concepts/state.html#subscribing-to-the-state) and [store. OnAction:

pinia.use(({ store }) => {
  store.$subscribe(() => {
    // react to store changes
  })
  store.$onAction(() => {
    // react to store actions
  })
})
Copy the code

Add new options#

You can create new options when you define a Store so that you can use them later from your plug-in. For example, you can create a debounce option that allows you to dejitter any operation:

defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // this will be read by a plugin later on
  debounce: {
    // debounce the action searchContacts by 300ms
    searchContacts: 300,
  },
})
Copy the code

The plug-in can then read this option to wrap the operation and replace the original operation:

// use any debounce library import debounce from 'lodash/debunce' pinia.use(({ options, store }) => { if (options.debounce) { // we are overriding the actions with new ones return Object.keys(options.debounce).reduce((debouncedActions, action) => { debouncedActions[action] = debounce( store[action], options.debounce[action] ) return debouncedActions }, {})}})Copy the code

Note that the custom option is passed as the third argument when using the set syntax:

defineStore(
  'search',
  () => {
    // ...
  },
  {
    // this will be read by a plugin later on
    debounce: {
      // debounce the action searchContacts by 300ms
      searchContacts: 300,
    },
  }
)
Copy the code

TypeScript#

Everything shown above can be done by typing support, so you don’t need to use any or @TS-ignore.

Typing the plugins plugin#

The Pinia plug-in can be typed as follows:

import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {
  // ...
}
Copy the code

Type the new Store property#

When adding new properties to the Store, you should also extend the PiniaCustomProperties interface.

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomProperties {
    // by using a setter we can allow both strings and refs
    set hello(value: string | Ref<string>)
    get hello(): string

    // you can define simpler values too
    simpleNumber: number
  }
}
Copy the code

It can then be safely written and read:

pinia.use(({ store }) => {
  store.hello = 'Hola'
  store.hello = ref('Hola')

  store.simpleNumber = Math.random()
  // @ts-expect-error: we haven't typed this correctly
  store.simpleNumber = ref(Math.random())
})
Copy the code

PiniaCustomProperties is a generic type that allows you to reference Store properties. Imagine the following example where we copy the initial option as $options (this only applies to option storage) :

pinia.use(({ options }) => ({ $options: options }))
Copy the code

We can enter PiniaCustomProperties correctly by using four generic types:

import 'pinia' declare module 'pinia' { export interface PiniaCustomProperties<Id, S, G, A> { $options: { id: Id state? : () => S getters? : G actions? : A } } }Copy the code

prompt

When extending types in generics, they must be named exactly as they are in the source code. Id cannot name Id or I, and S cannot name State. Here’s what each letter means:

  • S: State
  • G: Getters
  • A: Actions
  • SS: Setup Store / Store

Enter a new state#

When adding a new state properties (store and store. $state), you need to add the type to PiniaCustomStateProperties instead. Unlike PiniaCustomProperties, which only accepts State generics:

import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomStateProperties<S> {
    hello: string
  }
}
Copy the code

Type the new create option#

When creating a new option for defineStore(), you should extend DefineStoreOptionsBase. PiniaCustomProperties, in contrast, exposes only two generics: the State and Store types, allowing you to restrict what can be defined. For example, you can use the name of the operation:

import 'pinia' declare module 'pinia' { export interface DefineStoreOptionsBase<S, Store> { // allow defining a number of ms for any of the actions debounce? : Partial<Record<keyof StoreActions<Store>, number>> } }Copy the code

prompt

There is also a type that extracts getters from the Store type StoreGetters. You can also extend the options for setting Store or option Store simply by extending type and respectively. ****DefineStoreOptions“DefineSetupStoreOptions

Nuxt.js#

To use Pinia alongside Nuxt, you must first create a Nuxt plug-in. This will give you access to the Pinia instance:

// plugins/myPiniaPlugin.js import { PiniaPluginContext } from 'pinia' import { Plugin } from '@nuxt/types' function MyPiniaPlugin({ store }: PiniaPluginContext) {store.$subscribe((mutation) => {// react to store changes console.log(' 🍍 ${mutation. StoreId}]: ${mutation.type}.`) }) // Note this has to be typed if you are using TS return { creationTime: new Date() } } const myPlugin: Plugin = ({ $pinia }) => { $pinia.use(MyPiniaPlugin) } export default myPluginCopy the code

Note that the above example uses TypeScript, and if you are using a file, you must remove the type annotation PiniaPluginContext and its import. Plugin“.js

Use storage outside of components

Pinia Store relies on Pinia instances to share the same Store instance in all calls. UseStore () most of the time, just call your function out of the box. For example, setup() in, you don’t need to do anything else. But outside of the component, things are a little different. Behind the scenes, useStore() provides pinia to you to the app. This means that if Pinia cannot inject an instance automatically, it must be provided manually to the useStore() function. You can solve this problem in different ways depending on the type of application you are writing.

Single-page applications#

If you haven’t done any SSR (server-side rendering), any calls to app.use(pinia) after useStore() installs the Pinia plugin will work:

Import {useUserStore} from '@/stores/user' import {createApp} from 'vue' import App from './ app.vue '// ❌ fails because it's called before the pinia is created const userStore = useUserStore() const pinia = createPinia() const app = CreateApp (App) app.use(pinia) // ✅ works because the pinia instance is now active const userStore = useUserStore()Copy the code

The easiest way to ensure that this feature is always applied is to defer the call by putting it in a function that will always run after Pinia is installed. useStore()

Let’s take a look at this example using the Vue Router navigation guard inside the Store:

import { createRouter } from 'vue-router' const router = createRouter({ // ... }) // ❌ Depending on the order of imports this will fail const store = useStore() router. BeforeEach ((to, from,) next) => { // we wanted to use the store here if (store.isLoggedIn) next() else next('/login') }) router.beforeEach((to) => {// ✅ This will work because the router starts its navigation after // The router is installed and pinia will be installed too const store = useStore() if (to.meta.requiresAuth && ! store.isLoggedIn) return '/login' })Copy the code

SSR application#

When dealing with server-side rendering, you must pass the Pinia instance to useStore(). This prevents Pinia from sharing global state between different application instances.

There is a special section in the SSR guide and this is just a brief explanation:

Server-side Rendering (SSR)

prompt

If you are using nuxt.js, you need to read these instructions.

As soon as you call the function at the top of the useStore() function, creating a Store with Pinia can immediately use SSR setup, getters, and actions:

export default defineComponent({
  setup() {
    // this works because pinia knows what application is running inside of
    // `setup()`
    const main = useMainStore()
    return { main }
  },
})
Copy the code

Use Store outsidesetup()#

If you need to useStore elsewhere, you pass the instance passed to the application pinia to the function call: useStore()

Const pinia = createPinia() const app = createApp(app) app.use(router) app.use(pinia) router. BeforeEach ((to) => {// ✅ This will work make sure the correct store is used for the // current running app const main = useMainStore(pinia) if (to.meta.requiresAuth && ! main.isLoggedIn) return '/login' })Copy the code

Pinia conveniently adds $Pinia itself to your application, so you can use it serverPrefetch() in the following functions:

export default {
  serverPrefetch() {
    const store = useStore(this.$pinia)
  },
}
Copy the code

State hydration#

To hydrate the initial state, you need to ensure that the rootState is included somewhere in the HTML so that Pinia can retrieve it later. Based on what you use for SSR, you should escape this state for security reasons. We recommend using @nuxt/ devalue as used by nuxt.js:

import devalue from '@nuxt/devalue'
import { createPinia } from 'pinia'
// retrieve the rootState server side
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)

// after rendering the page, the root state is build and can be read directly
// on `pinia.state.value`.

// serialize, escape (VERY important if the content of the state can be changed
// by the user, which is almost always the case), and place it somewhere on
// the page, for example, as a global variable.
devalue(pinia.state.value)
Copy the code

Depending on what you use for SSR, you will set the initial state variables that will be serialized in HTML. You should also protect yourself from XSS attacks. For example, with Vite-SSR you can use the transformState option and @nuxt/devalue:

import devalue from '@nuxt/devalue'

export default viteSSR(
  App,
  {
    routes,
    transformState(state) {
      return import.meta.env.SSR ? devalue(state) : state
    },
  },
  ({ initialState }) => {
    // ...
    if (import.meta.env.SSR) {
      // this will be stringified and set to window.__INITIAL_STATE__
      initialState.pinia = pinia.state.value
    } else {
      // on the client side, we restore the state
      pinia.state.value = initialState.pinia
    }
  }
)
Copy the code

You can use other alternatives @nuxt/devalue as needed, for example, if you can serialize and parse your state json.parse () using json.stringify ()/, you can greatly improve your performance.

Adapt this policy to your environment. UseStore () makes sure to hydrate pinia’s state before calling any functions on the client side. For example, if we serialized the state to a

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// must be set by the user
if (isClient) {
  pinia.state.value = JSON.parse(window.__pinia)
}
Copy the code

Nuxt.js#

Pinia is easier to use with Nuxt.js because Nuxt handles a lot of things in terms of server-side rendering. For example, you don’t need to worry about serialization or XSS attacks.

The installation#

Make sure it’s installed next to: @nuxtjs/composition-apipinia

yarn add pinia @pinia/nuxt @nuxtjs/composition-api # or with npm npm install pinia @pinia/nuxt @nuxtjs/composition-api #  or with pnpm pnpm install pinia @pinia/nuxt @nuxtjs/composition-apiCopy the code

We provide a module to handle everything for you, you just need to add it to buildModules your nuxt.config.js file:

// nuxt.config.js
export default {
  // ... other options
  buildModules: [
    // Nuxt 2 only:
    // https://composition-api.nuxtjs.org/getting-started/setup#quick-start
    '@nuxtjs/composition-api/module',
    '@pinia/nuxt',
  ],
}
Copy the code

That’s it, use your Store as usual!

Use Store outsidesetup()#

If you want to use a store setup() outside, remember to pass the pinia object to useStore(). We add it to the context asyncData(), so you can access it fetch() in and:

import { useStore } from '~/stores/myStore'

export default {
  asyncData({ $pinia }) {
    const store = useStore($pinia)
  },
}
Copy the code

Use the Nuxt context in Store#

You can also use the context $nuxt in any Store by using the injection property:

import { useUserStore } from '~/stores/userStore' defineStore('cart', { actions: { purchase() { const user = useUserStore() if (! user.isAuthenticated()) { this.$nuxt.redirect('/login') } }, }, })Copy the code

Use Pinia with Vuex#

It is recommended to avoid using Pinia and Vuex together, but if you need to use both, you need to tell Pinia not to disable it:

// nuxt.config.js
export default {
  buildModules: [
    '@nuxtjs/composition-api/module',
    ['@pinia/nuxt', { disableVuex: false }],
  ],
  // ... other options
}
Copy the code

TypeScript#

If you use TypeScript or have jsconfig.json, you should also add the following type context.pinia:

{
  "types": [
    // ...
    "@pinia/nuxt"
  ]
}
Copy the code

This will also ensure that you have automatic completion 😉.

Do not usesetup()#

Pinia can be used even if you don’t use the composite API (if you use Vue 2, you still need to install the @vue/ composition-API plug-in). While we recommend that you try the Composition API and learn it, it may not be the time for you and your team, you may be migrating your application or for any other reason. It has several functions:

  • mapStores
  • mapState
  • mapWritableState
  • ⚠ ️mapGetters (just for migration convenience, use mapState() instead)
  • mapActions

Grants access to the entire Store#

If you need access to almost everything in the Store, there might be too many maps for each property of the Store…… Instead, you can access the entire StoremapStores() by:

import { mapStores } from 'pinia'

// given two stores with the following ids
const useUserStore = defineStore('user', {
  // ...
})
const useCartStore = defineStore('cart', {
  // ...
})

export default {
  computed: {
    // note we are not passing an array, just one store after the other
    // each store will be accessible as its id + 'Store'
    ...mapStores(useCartStore, useUserStore)
  },

  methods: {
    async buyStuff() {
      // use them anywhere!
      if (this.userStore.isAuthenticated()) {
        await this.cartStore.buy()
        this.$router.push('/purchased')
      }
    },
  },
}
Copy the code

By default, Pania adds the “Store” suffix to each Store. Id You can customize this behavior by calling setMapStoreSuffix() :

import { createPinia, setMapStoreSuffix } from 'pinia'

// completely remove the suffix: this.user, this.cart
setMapStoreSuffix('')
// this.user_store, this.cart_store (it's okay, I won't judge you)
setMapStoreSuffix('_store')
export const pinia = createPinia()
Copy the code

TypeScript#

By default, all Map Assistants support autocomplete, so you don’t have to do anything. If you call setMapStoreSuffix() to change the “Store” suffix, you also need to add it to a TS file or somewhere in your file, global.d.ts. The most convenient place is where you call setMapStoreSuffix() :

import { createPinia, setMapStoreSuffix } from 'pinia'

setMapStoreSuffix('') // completely remove the suffix
export const pinia = createPinia()

declare module 'pinia' {
  export interface MapStoresCustomization {
    // set it to the same value as above
    suffix: ''
  }
}
Copy the code

warning

If you are using a TypeScript declaration file (such as global.d.ts), make sure the import ‘pinia’ exposes all existing types at the top of it.