Pinia State Management Tutorial

In this Vue tutorial we learn about the replacement for Vuex called Pinia.

We cover how to set up Pinia or add it to your project, how to create a Option or Setup store and how to access a store.

Lesson Project

If you want to follow along with the examples, you will need an app generated with Vite & vue-create that includes Pinia.

What is Pinia?

Pinia is the new state management system for VueJS that’s intended to replace Vuex.

It allows us to store global state to what’s known as a store. We can then access that state from anywhere in our application.

For example, let’s say we wanted to check whether or not a user’s logged in on multiple pages in the application. Typically, we’d have to use props and events to pass that data around.

With Pinia, we can add the authentication status to the store and then access it from any component that needs to check it.

How to add Pinia to a Vue project

We can add peenya to a project in one of two ways.

The first way is to scaffold a new project with either Vite or create-vue and select it to be included.

We’ll use Vite and run the following command.

Command:
npm create vite@latest project-name -- --template vue

note The flag at the end tells Vite that we want to create a Vue project and not something else like React or Svelte

From the options, we’ll choose to “Add Pinia for state management”.

Options:
√ Project name: ... my-vite-project
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... No / Yes

After the project’s been created, we can install all the dependencies with npm install.

Command: Install dependencies
npm init vite@latest

And then we’re ready to use Pinia.

The second options is to add Pinia to an existing project with the following command.

Command: Install Pinia
npm install pinia

Once it’s been installed we’ll need to create a pinia instance and add it to the app as a plugin in the main.js file.

To do that, we import the createPinia method from the pinia package and specify it as an argument to the use method on the app.

Example: src/main.js
import { createApp } from 'vue'
// import Pinia
import { createPinia } from 'pinia'
import App from './App.vue'

// create instance
const pinia = createPinia()
const app = createApp(App)

// add it as a plugin
app.use(pinia)
app.mount('#app')

How to create a store (Option/Setup)

There are two types of stores we can create, an option store or a setup store.

The option store is similar to a Vuex store where we create our state, getters, and actions, as options.

Example: Options store
export const useCounterStore = defineStore('CounterStore', {
  // data
  state: () => ({ counter: 0 }),
  // computed properties
  getters: {
    getCount: (state) => state.counter,
  },
  // methods
  actions: {
    incrementCount() {
      this.count++
    },
  },
})

We can think of state as the data option, getters as the computed option, and actions as the methods option.

If you already know Vuex, this should be familiar to you. The only difference is that mutations are now handled behind the scenes.

The setup store is the Composition API’s version.

Example: Setup store
export const useCounterStore = defineStore('CounterStore', () => {
  // state
  const counter = ref(0)
  // getters
  const getCount = computed(() => count.value)
  // actions
  function incrementCount() {
    counter.value++
  }

  return { getCount, incrementCount }
})

In a setup store refs are the state , computeds are the getters and functions are the actions .

For our first example, we’ve created a simple store file.

  • src/store/counter.js

The project should look similar to the following.

Project:
project-name/
├── src/
|   ├── store/
|   |   └── counter.js
|   └── App.vue

To create a store, we use the defineStore function, imported from the pinia package.

The function accepts two arguments.

  1. A unique string name
  2. A callback where we define our state, getters, and actions.

It’s typically stored in something like a constant that we can export and use in other components.

tip The naming convention for stores (and composables in general) is to prefix the name with “use”.

Syntax: defineStore
import { defineStore } from 'pinia'

export const useNameStore = defineStore('unique-store-name', () => {
  // state
  // getters
  // actions
})

To demonstrate, let’s add a store to our counter file.

Example: src/store/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('CounterStore', {
  
})

For the data, we’ll define a new ref and set it to 0.

Because we shouldn’t directly get or change data from outside the store, we’ll add a computed property that returns the counter value, as well as a function that increments the counter.

Finally, we’ll return the getter and action so we can use it around the app.

Example: src/store/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('CounterStore', () => {
  // state
  const counter = ref(0)
  // getter
  const getCounter = computed(() => counter.value)
  // action
  function incrementCounter() {
    counter.value++
  }

  return { getCounter, incrementCounter }
})

How to access a store

To access a store, we need to import and invoke it in a component.

Syntax: Instantiate the store
<script>
import { useNameStore } from '@/stores/storename.js'

export default {
  setup() {
    const store = useNameStore()
  }
}
</script>

Once the store is instantiated, we can access the data.

To keep the data reactive, we need to destructure the state and getters with the storeToRefs function. Actions can be destructured directly.

Example: Destructure state & getters
<script>
import { useNameStore } from '@/stores/storename.js'
import { storeToRefs } from 'pinia'

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

    // destructure 'refs' and 'getters'
    const { someRef, someGetter } = storeToRefs(store)
    // destructure 'actions'
    const { someAction } = store

    return {
      someRef, someGetter, someAction
    }
  }
}
</script>

To use the data, we can reference it in the template just like we normally would.

Example:
<script>
import { useNameStore } from '@/stores/storename.js'
import { storeToRefs } from 'pinia'

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

    const { someRef, someGetter } = storeToRefs(store)
    const { someAction } = store

    return {
      someRef, someGetter, someAction
    }
  }
}
</script>

<template>
  <h2>{{ someGetter }}</h2>
  <p>{{ someAction }}</p>
</template>

To demonstrate, we’ll access our counter store from the root App component.

We’ll start by importing the store and the storeToRefs function. Then we instantiate it in a constant called “store”.

After that we’ll destructure the getter and the action, so we can use it in the template.

Finally, we’ll use the action in a click event on the button, and show the counter’s value inside it.

Example:
<script>
  import { useCounterStore } from '@/stores/counter.js'
  import { storeToRefs } from 'pinia'

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

    const { getCounter } = storeToRefs(store)
    const { incrementCounter } = store

    return {
      getCounter, incrementCounter
    }
  },
}
</script>

<template>
  <button @click="incrementCounter">Increment counter: {{ getCounter }}</button>
</template>

If we run the example in the browser, the counter increments when we click the button.

Further Reading

For more information on the topics covered in this lesson, please see the relevant sections below.