How to Use Pinia for State Management in Vue 3

Modern Vue application development means juggling data across pages, modals, and components. State Management in Vue 3 Using Pinia makes that painless. In this Vue 3 Pinia guide, you’ll be setting up Pinia, registering it in your app, and learning with a friendly Pinia Vue 3 example. We’ll also create a simple auth store that you can drop into real projects.

What Is Pinia? Installation and Setup

Pinia is the official state management library for Vue and a lightweight replacement to Vuex. It provides a clean API, TypeScript support out-of-the-box, and great devtools. You declare a store, read reactive state, and call actions to change it. It feels natural and requires fewer concepts than Vuex.

Before moving forward, you need to install Pinia into Node.js Application.

npm install pinia

Here’s a simple example showing how to create a Pinia instance using createPinia and use it in your Vue 3 app:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

Why Pinia Over Vuex?

  • Simpler API: only actions and getters, no additional mutation layer
  • Faster learning curve: fewer concepts to memorize
  • TypeScript friendly: strong inference and autocomplete
  • Modular by default: one file per store keeps code organized
  • Great DX: hot module replacement, Vue Devtools support
  • SSR ready: works well with server-side rendering

Basic Pinia Store Component In Vue 3 Example

Let’s make a tiny counter store and consume it in a component. This mirrors a common real-life case like tracking cart items or unread notifications. we will start with creating store file src/stores/counter.js.

import { defineStore } from 'pinia'

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

Use it in a component like below example:

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
const { count, double } = storeToRefs(counter)

function addOne() {
  counter.increment()
}
</script>

<template>
  <div>
    <h3>Counter</h3>
    <p>Count: {{ count }}</p>
    <p>Double: {{ double }}</p>
    <button @click="addOne">Add</button>
    <button @click="counter.reset()">Reset</button>
  </div>
</template>

This demonstrate reactive values with this simple method. It’s a main thing into sample component.

Use Pinia in Vue 3 for Auth Store

Most apps need authentication. So let’s take an example handles login, logout, a token, and a user profile, with simple localStorage persistence so users stay signed in after refresh.

// src/stores/auth.js
import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: localStorage.getItem('token') || null
  }),
  getters: {
    isAuthenticated: (state) => !!state.token
  },
  actions: {
    async login(email, password) {
      const res = await fetch('https://example.com/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      })
      if (!res.ok) throw new Error('Invalid credentials')
      const data = await res.json()
      this.token = data.token
      this.user = data.user
      localStorage.setItem('token', this.token)
      localStorage.setItem('user', JSON.stringify(this.user))
    },
    loadFromStorage() {
      const userJson = localStorage.getItem('user')
      this.user = userJson ? JSON.parse(userJson) : null
      this.token = localStorage.getItem('token')
    },
    logout() {
      this.user = null
      this.token = null
      localStorage.removeItem('token')
      localStorage.removeItem('user')
    }
  }
})

Here, we have create store for authentication that is based on token. We have also defined getter method to get token. The methods login and logout will handle all the authentication process. However, we have also added for retrieving token from storage for checking user is authenticated or not.

Let’s create and form for login using this store to perform operations.

<script setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'

const auth = useAuthStore()
const email = ref('')
const password = ref('')
const error = ref('')
const loading = ref(false)

async function submit() {
  error.value = ''
  loading.value = true
  try {
    await auth.login(email.value, password.value)
  } catch (e) {
    error.value = 'Login failed'
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model="email" type="email" placeholder="Email" />
    <input v-model="password" type="password" placeholder="Password" />
    <button :disabled="loading">{{ loading ? 'Signing in...' : 'Sign In' }}</button>
    <p v-if="auth.isAuthenticated">Welcome, {{ auth.user?.name }}</p>
    <p v-if="error">{{ error }}</p>
  </form>
</template>

For logout, let’s add and link to header for user to able to click from anywhere.

<script setup>
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
</script>

<template>
  <header>
    <nav>
      <button v-if="auth.isAuthenticated" @click="auth.logout()">Logout</button>
      <router-link v-else to="/login">Login</router-link>
    </nav>
  </header>
</template>

It’s for minor application. You can improve this with store the token, show the user’s name, and allow logout. You can later swap the fetch URL for your API and the pattern stays the same.

Conclusion

Pinia keeps state management in Vue 3 simple and scalable. You installed it, registered it, and built a counter plus an auth store that works in real life. Whether you’re looking for a concise Vue 3 Pinia tutorial, this guide has shown how to use Pinia in Vue 3 without ceremony. Besides, starting with one store, keeping actions small, and letting Pinia take care of reactivity will do the job: as your app grows, your stores stay readable, and your team stays fast.