Pinia — Global State Management
In this chapter, you will learn to use Pinia to manage shared data across components and pages, and implement the "favorite articles" feature.
Why do we need global state management?
So far, data has been passed between components via props and emit.
This is sufficient for simple parent-child scenarios, but becomes cumbersome in the following situations:
- The favorite state needs to be synchronized between the home page and the detail page.
- The user login state needs to be shared across all pages.
- Shopping cart data needs to be passed between the navigation bar, product page, and checkout page.
Passing props layer by layerThe pain point: If data in component A needs to be passed to component D, it has to go through B and C as relays, even though these intermediate components do not need the data themselves.
PiniaPinia is the state management library officially recommended by Vue 3. It provides a global "data center" where any component can directly read and modify data without layer-by-layer passing.
Installing and Registering Pinia
If you already selected Pinia when creating the project, skip the installation steps.
Otherwise:
$ npm install pinia
Register it in main.js:
Example
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// Create a Pinia instance and register it
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
defineStore — Defining a Store
The core of Pinia is theStoreEach Store manages an independent piece of state.
It consists of three parts:
| Part | Purpose | Analogy |
|---|---|---|
| state | Store data | Component's data / ref |
| getters | Derived computed data | Component's computed |
| actions | Methods for modifying data | Component's methods / function |
Example
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// defineStore('storeName', () => { ... })
// The second parameter uses a function syntax, consistent with the Composition API style.
export const useFavoriteStore = defineStore('favorite', () => {
// ====== state: define state with ref ======
// Use an array to store the IDs of favorited articles
const favoriteIds = ref([])
// Restore the favorite list from localStorage
const saved = localStorage.getItem('blog-favorites')
if (saved) {
favoriteIds.value = JSON.parse(saved)
}
// ====== getters: define computed properties with computed ======
const favoriteCount = computed(() => favoriteIds.value.length)
// Determine whether an article has been favorited
function isFavorite(articleId) {
return favoriteIds.value.includes(articleId)
}
// ====== actions: define modification methods with regular functions ======
// Toggle favorite status (favorited → unfavorited, not favorited → favorited)
function toggleFavorite(articleId) {
const index = favoriteIds.value.indexOf(articleId)
if (index === -1) {
favoriteIds.value.push(articleId) // Add favorite
} else {
favoriteIds.value.splice(index, 1) // Remove favorite
}
// Sync to localStorage (persistent storage)
localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds.value))
}
return { favoriteIds, favoriteCount, isFavorite, toggleFavorite }
})
The second parameter of defineStore can be written in two ways:Options Store(similar to Vue 2's data/methods/computed object syntax) andSetup Store(a function syntax using the Composition API). It is recommended to use the Setup Store, which is consistent with the
<script setup>syntax used inside components, and has the lowest learning cost.
Using the Store in Components
Any component canuseFavoriteStore()obtain a store instance, and then directly read/write state and call actions.
Adding a Favorite Button in BlogCard
Example
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'
const props = defineProps({
id: Number,
title: String,
summary: String,
date: String,
category: String
})
const favoriteStore = useFavoriteStore()
// Called when the favorite button is clicked
function handleFavorite(e) {
e.preventDefault() // Prevent RouterLink from navigating
favoriteStore.toggleFavorite(props.id)
}
</script>
<template>
<RouterLink :to="{ name: 'post', params: { id } }" class="card-link">
<div class="card">
<span class="tag">{{ category }}</span>
<h3>{{ title }}</h3>
<p>{{ summary }}</p>
<div class="card-footer">
<span class="date">{{ date }}</span>
<!-- Favorite button: solid when favorited, outline when not favorited -->
<button class="fav-btn" @click="handleFavorite">
{{ favoriteStore.isFavorite(id) ? '♥' : '♡' }}
</button>
</div>
</div>
</RouterLink>
</template>
<style scoped>
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.fav-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #e74c3c;
padding: 4px 8px;
}
.fav-btn:hover {
transform: scale(1.2);
}
</style>
Also Add a Favorite Button on the Detail Page
Example
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'
const favoriteStore = useFavoriteStore()
// ... other code
</script>
<template>
<article v-if="article">
<div class="post-header">
<h1>{{ article.title }}</h1>
<!-- Favorite button on the detail page: state fully synchronized with the home page -->
<button class="fav-btn" @click="favoriteStore.toggleFavorite(article.id)">
{{ favoriteStore.isFavorite(article.id) ? '♥Favorited' : '♡ Favorite' }}
</button>
</div>
<!-- ... -->
</article>
</template>
The favorite state on the home page and detail page is fully synchronized: after favoriting an article on the home page, entering the detail page automatically shows "Favorited".
This is the value of global state management: data is stored in a single place, and all components read and write to the same source. There is no need for emit passing or layer-by-layer props forwarding.
Displaying the Favorite Count in NavBar
Display the favorite count on the right side of the navigation bar; click it to see the list of favorited articles.
Example
<script setup>
import { useFavoriteStore } from '../stores/useFavoriteStore.js'
const favoriteStore = useFavoriteStore()
</script>
<template>
<header class="navbar">
<a href="/" class="logo">EXAMPLE Blog</a>
<nav>
<!-- Favorite count badge -->
<span class="fav-badge" v-if="favoriteStore.favoriteCount > 0">
Favorites {{ favoriteStore.favoriteCount }}
</span>
</nav>
</header>
</template>
<style scoped>
.fav-badge {
background: #e74c3c;
color: #fff;
padding: 4px 10px;
border-radius: 12px;
font-size: 13px;
}
</style>
The count in NavBar automatically stays synchronized with the latest favorite list — favoriting or unfavoriting an article immediately changes the number in the navigation bar.
Summary of Pinia's Three Core Capabilities
| Capability | Corresponding Code | Application in the Favorite Feature |
|---|---|---|
| Global Sharing | Any component can use useXxxStore() | NavBar, BlogCard, and PostView share the same favorite list. |
| Reactivity | When state changes, all consumers automatically update. | After favoriting, the home page button turns red, the navigation bar count increases by 1, and the detail page updates synchronously. |
| Persistence | Write to localStorage in actions | The favorite list still exists after refreshing the page. |
Chapter Summary
In this chapter, you have mastered the complete usage of Pinia for global state management: using defineStore to define a store (state/getters/actions), using useXxxStore in components to get an instance, and implementing data persistence through localStorage.
The "favorite articles" feature makes data synchronization between the home page, detail page, and navigation bar very natural, and all components automatically stay consistent.
Other Extensions