Pinia

Pinia

Pinia (pronounced like /peenya/ in English) is the closest word to piña (pineapple in Spanish) that is a valid package name. A pineapple is in reality a group of individual flowers that join together to create a multiple fruit. Similar to stores, each one is born individually, but they are all connected at the end. It’s also a delicious tropical fruit indigenous to South America.

Not all applications need access to a global state, but if yours need one, Pinia will make your life easier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 })
    },
  },
})

认识

1
2
3
import { createPinia } from 'pinia';

app.use(createPinia());

start by creating a store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// stores/counter.js
import { defineStore } from 'pinia'

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

use it in a component

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counter = useCounterStore()

    counter.count++
    // with autocompletion ✨
    counter.$patch({ count: counter.count + 1 })
    // or using an action instead
    counter.increment()
  },
}

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

1
2
3
4
5
6
7
8
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

安装

安装需要 @next 因为 Pinia 2 处于 beta 阶段, Pinia 2 是对应 Vue3 的版本

1
2
3
4
# 使用 npm
npm install pinia@next
# 使用 yarn
yarn add pinia@next

Store

A Store (like Pinia) is an entity holding state and business logic that isn’t bound to your Component tree.

In other words, it hosts global state.

It’s a bit like a component that is always there and that everybody can read off and write to.

It has three concepts, the state, getters and actions and it’s safe to assume these concepts are the equivalent of data, computed and methods in components.

Defining a Store

1
2
3
4
5
6
7
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...
})

Using the store

We are defining a store because the store won’t be created until useStore() is called inside of setup():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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,
    }
  },
}

Note that store is an object wrapped with reactive, meaning there is no need to write .value after getters but, like props in setup, we cannot destructure (解构) it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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),
      }
  },
})

In order to extract properties from the store while keeping its reactivity, you need to use storeToRefs(). It will create refs for any reactive property. This is useful when you are only using state from the store but not calling any action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)

    return {
      name,
      doubleCount
    }
  },
})

State

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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,
    }
  },
})

Accessing

By default, you can directly read and write to the state by accessing it through the store instance:

1
2
3
const store = useStore()

store.counter++

Resetting

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

1
2
3
const store = useStore()

store.$reset()

Mutating

Apart from directly mutating the store with store.counter++, you can also call the $patch method. It allows you to apply multiple changes at the same time with a partial state object:

1
2
3
4
store.$patch({
  counter: store.counter + 1,
  name: 'Abalam',
})

However, some mutations are really hard or costly to apply with this syntax: any collection modification (e.g. pushing, removing, splicing an element from an array) requires you to create a new collection. Because of this, the $patch method also accepts a function to group this kind of mutations that are difficult to apply with a patch object:

1
2
3
4
cartStore.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

The main difference here is that $patch() allows you to group multiple changes into one single entry in the devtools. Note both, direct changes to state and $patch() appear in the devtools and can be time travelled (not yet in Vue 3).

Replacing

You can replace the whole state of a store by setting its $state property to a new object:

1
store.$state = { counter: 666, name: 'Paimon' }

You can also replace the whole state of your application by changing the state of the pinia instance. This is used during SSR for hydration

1
pinia.state.value = {}

Subscribing

You can watch the state and its changes through the $subscribe() method of a store, similar to Vuex’s subscribe method. The advantage of using $subscribe() over a regular watch() is that subscriptions will trigger only once after patches (e.g. when using the function version from above).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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))
})

Getters

Getters are exactly the equivalent of computed values for the state of a Store. They can be defined with the getters property in defineStore(). They receive the state as the first parameter to encourage the usage of arrow function:

1
2
3
4
5
6
7
8
export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
  },
})

Most of the time, getters will only rely on the state, however, they might need to use other getters. Because of this, we can get access to the whole store instance through this when defining a regular function but it is necessary to define the type of the return type (in TypeScript). This is due to a known limitation in TypeScript and doesn’t affect getters defined with an arrow function nor getters not using this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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.counter * 2 + 1
    },
  },
})

Then you can access the getter directly on the store instance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>

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

    return { store }
  },
}
</script>

Accessing other getters

As with computed properties, you can combine multiple getters. Access any other getter via this. Even if you are not using TypeScript, you can hint your IDE for types with the JSDoc:

绷不住了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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
    },
  },
})

Accessing other stores getters

To use another store getters, you can directly use it inside of the getter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { useOtherStore } from './other-store'

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

Actions

Actions are the equivalent of methods in components. They can be defined with the actions property in defineStore() and they are perfect to define business logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  actions: {
    increment() {
      this.counter++
    },
    randomizeCounter() {
      this.counter = Math.round(100 * Math.random())
    },
  },
})

Actions are invoked like methods:

1
2
3
4
5
6
7
8
9
export default defineComponent({
  setup() {
    const main = useMainStore()
    // call the action as a method of the store
    main.randomizeCounter()

    return {}
  },
})

Accessing other stores actions

To use another store, you can directly use it inside of the action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { useAuthStore } from './auth-store'

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

File Structure

1
2
3
4
5
6
7
8
# Pinia equivalent, note ids match previous namespaces
src
└── stores
    ├── index.js          # (Optional) Initializes Pinia, does not import stores
    ├── module1.js        # 'module1' id
    ├── nested-module2.js # 'nested/module3' id
    ├── nested-module3.js # 'nested/module2' id
    └── nested.js         # 'nested' id