1. Goals of Pinia

  • Each component has its own set of local variables, defined by using ref() or reactive()
  • It is however possible to use "external" data:
    • via the import instruction,
    • via props and slots.
  • Now, it is common to dynamically create/destroy components, notably with vue-router (which is covered by Reference #5), or with various other vuejs mechanisms.
  • When a component is destroyed, its own data is lost but not the data whose source is external, unless of course that source is a component which is itself destroyed.
  • To make data persist despite the destruction of components, this data must therefore be stored in a location that will never be destroyed.
  • For this principle to be practical, it is enough to create a central "repository" for the data model, which all the components of the application have access to, as soon as they are created.
  • The other advantage of a central repository is to facilitate the passing of information between components.
  • Indeed, passing data between 2 sibling components A and B requires:
    • that A triggers events with values representing the data to communicate,
    • that A's parent captures these events and assigns their value to some of its variables.
    • that A's parent links these variables to props of B.
  • If the communication must happen between cousins, it is even more complicated since events must be propagated up and props passed down over several levels.
  • A central repository completely avoids this kind of mechanism since it is enough to have a variable in the repository containing the data to communicate.

 

  • Pinia lets you create such a repository, called the "store", in only a few lines of code, and access it in components by adding 2 instructions.
  • It is thus possible to write in a few lines an object containing the centralized data and to import this object into all components via import.

 

WARNING! Just because you use Pinia does not mean you are going to put all the data in the store. Components can still define local variables, if they are not needed by other components.

 

2. Basic principles of Pinia

  • When you create a blank project thanks to vite, in which you integrate pinia (via the project customization menu), a stores directory is created with a counter.js file inside.
  • This file is just an example of a store holding a counter and it can be freely deleted and replaced by one or more files that will define the application's store.
  • Indeed, pinia lets you create as many stores as you want, each in a separate file, rather than a single "big" repository.
  • Components can thus import only the stores they need.
  • A pinia store mainly comprises:
    • a set of variables defining the "state" of the store, that is the information you want to make accessible to all components.
    • "action" functions allowing these variables to be updated (NB: they can be asynchronous and therefore call a remote API)
  • A store can also define "getters", in the form of computed variables, which will also be accessible to the components that import the store.

 

  • Note that there are different syntaxes for defining the state, the getters and the actions. However, the simplest is to use the one that mirrors the syntax used in the <script setup> parts of components.

  

3. Creating a store

 

3.1. The creation function

  • Creating a store starts by calling pinia's defineStore() function. It takes 2 parameters:
    • the name of the store,
    • a function (without parameters) whose code defines the store, and which must return the elements you want to make accessible to components.
  • For example:
import { defineStore } from 'pinia'
export const useMyStore = defineStore('mystore', () => {
  // store definition
})
  • You can see that this function itself returns a function that must be stored in an exported variable.
  • It is this variable that components will have to import.
  • NB: the name of this variable is free, but it is customary to name it useXXXStore.

 

3.2. definition of the store

  • The code of the arrow function given as a parameter to defineStore() can be written in different ways but the simplest is to use a syntax equivalent to the script part of components.
  • The only difference is that this code must end with a return, which must return an object containing the list of elements (state, getters and actions) that you want to make accessible.
  • It is a bit like the principle of exporting
  • NB: a common mistake is to add elements to a store and forget to add them to the return. This necessarily causes errors when a component wants to access an element of the store that has not been returned.

 

  • To define state elements, you define variables with the ref() function (or possibly reactive() but this is not necessarily advised)
  • To define getters, you use the computed() function
  • To define actions, you simply use the function keyword.

Remarks:

  • To use a state variable in a getter or an action, you use var_name.value (so as in script)
  • You can use a getter in another getter, with getter_name.value
  • An action can take parameters.
  • An action can be asynchronous, which notably allows it to call an API.
  • You can create auxiliary functions used by the getters/actions, without returning them at the end. These correspond to a kind of functions private to the store.

 

Example (in stores/count.js):

import { defineStore } from 'pinia'
import {ref, computed} from 'vue'
export const useCountStore = defineStore('countstore', () => {
  // STATE
  const count = ref(0)
  // GETTERS
  const doubleCount = computed( () => 2*count.value)
  const fourthCount = computed( () => 2*doubleCount.value)
  // ACTIONS
  function incCount(amount) {
    count.value += amount
  }
  return { count, doubleCount, fourthCount, incCount }
})
 

4. accessing the store in components

4.1. setting up access

  • A component that wants to use a store only has to, in <script setup>:
    • import the useXXXStore() function of the desired store,
    • call this function to retrieve an object allowing access to the store.
  • The naming of this object is free, but custom dictates calling it xxxStore

Example:

<script setup>
import {useCountStore} from '@/stores/count.js'
const countStore = useCountStore()
...
</script>

 

4.2. Manipulating the store

  • Accessing the members of the store in a component is done the same way in the template AND in the script.
  • To manipulate a state variable: store_name.variable_name,
  • To get the value of a getter: store_name.getter_name,
  • To call an action: store_name.action_name(...).
  • You will therefore notice that you NEVER use .value, despite the fact that you are accessing observed variables.

Example:

<template>
  <div>
      {{ countStore.count }} {{ countStore.doubleCount }}
      <button @click=countStore.incCount(5)>Inc by 5</button>
      <button> @click="reset">Reset</button>
  </div>
</template>
<script setup>
import {useCountStore} from '@/stores/count.js'
const countStore = useCountStore()
function reset() {
  countStore.incCount( -countStore.count) // example of calling an action, would be simpler to do countStore.count = 0
  console.log(countStore.doubleCount)
}
</script>

 

4.3. Subtleties

 

4.3.1. Getters with a parameter

  •  Getters are created with computed() and therefore cannot by default have parameters.
  • However, you can use a trick: the computed function does not return a value but a function with a parameter, which you can then call.

Example (in the store count.js) :

const multBy = computed( () => amount => count.value*amount)

 Example of use in the template of a component:

{{ countStore.multBy(3) }}
  • This works thanks to the fact that JS is an interpreted language:
    • it first evaluates countStore.multBy, which leads to "replacing" it with its value, which is amount => count.value*amount (so a function)
    • it then evaluates (amount => count.value*amount)(3), which leads to executing this function with 3 as a parameter.

4.3.2. "External" access to a store

  • It is sometimes useful to access the store from something other than a component, for example a service function, in the router, ...
  • In this case, the only constraint is to make sure that Pinia has been initialized before accessing the store (NB: This initialization is generally done in the main.js file, more or less directly).
  • Making sure this constraint is respected is not very complicated:
    • you spot any file containing an instruction of the type useXXXStore(),
    • you check where this instruction is located and when it will be executed.
    • If it is executed, directly or indirectly, as soon as the file is imported => potential problem if the import happens before pinia is initialized.
    • In this case, you get a message in the console:
  • "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?
  • If you are in the situation above, it is enough to move the instruction so that it is not executed when the file is imported but only when necessary.
  • An example of such a solution is given in the demonstration.

4.3.3. Hiding the use of the store in the template

  •  Using the name of a store in a template makes this template dependent on the use of pinia.
  • Its code can therefore not be directly reused to create a visually similar component but with a different script part.
  • It is however possible to hide this use by creating computed variables and functions that will be used in the template, but which in fact, allow access to the store.

Example:

<template>
      {{ count }}
      <button @click=inc(5)>Inc by 5</button>
</template>
<script setup>
import {useCountStore} from '@/stores/count.js'
import {computed} from 'vue'
const countStore = useCountStore()

const count = computed(() => countStore.count)

function inc(amount) {
  countStore.incCount(amount)
}
</script>
  • You will notice that there is no longer any reference to the store in this template.