Tutoriel Pinia pour débutants
Dans Vue3, la gestion d'état est un sujet incontournable.
Pinia est une bibliothèque légère de gestion d'état pour Vue.js qui vous permet de partager et de gérer l'état entre les composants. On peut considérer Pinia comme un entrepôt de données global dans lequel tous les composants peuvent récupérer ou mettre à jour des données.
Dans ce chapitre, nous présentons Pinia, la bibliothèque de gestion d'état officiellement recommandée pour Vue3. Comparé à Vuex, Pinia offre une solution de gestion d'état plus simple qui correspond mieux à l'esprit de la Composition API de Vue3.
Le diagramme suivant compare les différences structurelles entre les deux :
- Vuex utilise un seul store avec des modules à plusieurs niveaux dans une structure arborescente, avec une hiérarchie fixe, s'appuie sur des mutations et est globalement plus lourd.
- Pinia, en revanche, se compose de plusieurs stores indépendants ; il est plat, léger, sans séparation en modules et sans surcoût de namespace.

Fonctionnalités principales de Pinia :
- Prend en charge Vue2 et Vue3
- Conception d'API minimaliste
- Prise en charge complète de TypeScript
- Prend en charge la Composition API
- Conception modulaire sans modules imbriqués
La structure plate de Pinia permet aux composants de se connecter directement à n'importe quel store et de prendre ce dont ils ont besoin, sans passer par des modules arborescents ni des chemins de namespace. Le flux d'état est simple et direct, avec un faible couplage.

Installation et configuration
Créer un projet avec Vite :
npm create vite@latest vue-pinia-demo --template vue cd vue-pinia-demo npm install
Installer Pinia
Tout d'abord, installez Pinia dans votre projet Vue3 :
npm install pinia # 或者 yarn add pinia
Pinia comporte trois parties :
- state : stocke les données
- getters : calcule les données dérivées
- actions : exécute la logique, modifie le state
Configurer Pinia
Configurer Pinia dans src/main.js ou src/main.ts :
Exemple
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// Créer l'instance Pinia
const pinia = createPinia()
// Créer l'application Vue
const app = createApp(App)
// Utiliser Pinia
app.use(pinia)
app.mount('#app')
Créer votre premier store
Un store est l'entrepôt de données dans Pinia. Créons un store de compteur simple.
Définir un store
Créer un nouveau fichier src/stores/useCounter.js :
Exemple
import { defineStore } from 'pinia'
// Définir un store avec defineStore
// Le premier paramètre est l'ID unique du store
// Le second paramètre correspond aux options de configuration du store
export const useCounterStore = defineStore('counter', {
// state : définit les données d'état du store
state: () => ({
count: 0,
name: 'Mon compteur'
}),
// getters : définit des propriétés calculées à partir de state
getters: {
doubleCount: (state) => state.count * 2,
// Utilisez ceci pour accéder aux autres getters
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
// actions : définit des méthodes pour modifier l'état
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
// Peut accepter des paramètres
incrementBy(amount) {
this.count += amount
},
// Action asynchrone
async incrementAsync() {
// Simuler une opération asynchrone
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
Explication de la structure du store
Comprenons les différentes parties d'un store à travers un tableau :
| Partie | Objectif | Exemple |
|---|---|---|
| state | Définit les données stockées | count: 0 |
| getters | Propriétés calculées en fonction de l'état | doubleCount: state => state.count * 2 |
| actions | Méthodes pour modifier l'état | increment() { this.count++ } |
Utilisation du Store dans les composants
Utilisation de base
Le code du composant pour src/components/CounterComponent.vue est le suivant :
Exemple
<template>
<div class="counter">
<h3>{{ store.name }}</h3>
<p>Nombre actuel : {{ store.count }}</p>
<p>Nombre doublé : {{ store.doubleCount }}</p>
<p>Nombre doublé plus un : {{ store.doubleCountPlusOne }}</p>
<button @click="store.increment()">+1</button>
<button @click="store.decrement()">-1</button>
<button @click="store.incrementBy(5)">+5</button>
<button @click="store.incrementAsync()">Asynchrone +1</button>
<button @click="reset">Réinitialiser</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/useCounter'
// Utiliser le store dans setup
const store = useCounterStore()
// Méthode pour réinitialiser l'état
function reset() {
store.$reset() // La méthode $reset peut réinitialiser l'état à sa valeur initiale
}
</script>
Modifier src/App.vue comme suit :
Exemple
import CounterComponent from './components/CounterComponent.vue'
</script>
<template>
<CounterComponent />
</template>
La structure complète du projet :

Exécuteznpm run devcommande, et visitez dans le navigateurhttp://localhost:5173/, et voyez l'effet :

Déstructuration réactive
Si vous voulez utiliser directement les propriétés d'état dans le template, vous pouvez utiliserstoreToRefs:
Exemple
<div>
<p>Nombre : {{ count }}</p>
<p>Nom : {{ name }}</p>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Utilisez storeToRefs pour conserver la réactivité
const { count, name } = storeToRefs(store)
// Remarque : la déstructuration directe perdra la réactivité !
// Mauvaise façon : const { count, name } = store
</script>
Store de style Composition API
Pinia prend également en charge la définition de stores en utilisant le style Composition API :
Exemple
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// état
const user = ref(null)
const isLoggedIn = ref(false)
// accesseurs
const userName = computed(() => user.value?.name || 'Invité')
const userAge = computed(() => user.value?.age || 0)
// actions
function login(userData) {
user.value = userData
isLoggedIn.value = true
}
function logout() {
user.value = null
isLoggedIn.value = false
}
return {
user,
isLoggedIn,
userName,
userAge,
login,
logout
}
})
Interaction entre les stores
Plusieurs stores peuvent s'appeler entre eux:
Exemple
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
addItem(product) {
this.items.push(product)
// Appeler une action depuis un autre store
const userStore = useUserStore()
if (userStore.isLoggedIn) {
// Synchroniser avec le panier d'achat de l'utilisateur
this.syncToUserCart()
}
}
}
})
Exemple pratique : Panier d'achat
Créons un exemple complet de panier d'achat :
Exemple
export const useProductStore = defineStore('products', {
state: () => ({
products: [
{ id: 1, name: 'Laptop', price: 5999, stock: 10 },
{ id: 2, name: 'Smartphone', price: 3999, stock: 20 },
{ id: 3, name: 'Écouteurs sans fil', price: 299, stock: 50 }
]
}),
getters: {
// Obtenir le produit par ID
getProductById: (state) => (id) => {
return state.products.find(product => product.id === id)
},
// Obtenir les produits en stock
availableProducts: (state) => {
return state.products.filter(product => product.stock > 0)
}
}
})
Exemple
export const useCartStore = defineStore('cart', {
state: () => ({
items: [], // { productId, quantity }
discount: 0
}),
getters: {
// Nombre total d'articles dans le panier
totalItems: (state) => {
return state.items.reduce((total, item) => total + item.quantity, 0)
},
// Montant total du panier
totalPrice: (state) => {
const productStore = useProductStore()
return state.items.reduce((total, item) => {
const product = productStore.getProductById(item.productId)
return total + (product?.price || 0) * item.quantity
}, 0)
},
// Prix total après réduction
finalPrice: (state) => {
return state.totalPrice * (1 - state.discount / 100)
}
},
actions: {
// Ajouter un produit au panier
addToCart(productId, quantity = 1) {
const existingItem = this.items.find(item => item.productId === productId)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({ productId, quantity })
}
},
// Supprimer le produit du panier
removeFromCart(productId) {
this.items = this.items.filter(item => item.productId !== productId)
},
// Vider le panier
clearCart() {
this.items = []
this.discount = 0
},
// Définir la remise
setDiscount(percent) {
this.discount = Math.max(0, Math.min(100, percent))
}
}
})
Utilisation du panier d'achat dans un composant
Exemple
<template>
<div class="shopping-cart">
<h3>Panier d'achat ({{ cart.totalItems }} articles)</h3>
<div v-if="cart.items.length === 0">
<p>Le panier est vide</p>
</div>
<div v-else>
<div v-for="item in cartItems" :key="item.product.id" class="cart-item">
<span>{{ item.product.name }}</span>
<span>¥{{ item.product.price }}</span>
<span>Quantité : {{ item.quantity }}</span>
<span>Sous-total : ¥{{ item.product.price * item.quantity }}</span>
<button @click="cart.removeFromCart(item.product.id)">Supprimer</button>
</div>
<div class="cart-summary">
<p>Total : ¥{{ cart.totalPrice }}</p>
<p v-if="cart.discount >0">Remise : {{ cart.discount }}%</p>
<p>Montant payé : ¥{{ cart.finalPrice }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/products'
const cart = useCartStore()
const products = useProductStore()
// Calculer les détails des articles du panier
const cartItems = computed(() => {
return cart.items.map(item => ({
product: products.getProductById(item.productId),
quantity: item.quantity
})).filter(item => item.product) // Filtrer les produits qui n'existent pas
})
</script>
Bonnes pratiques et considérations
1. Conventions de nommage des stores
Exemple
export const useUserStore = defineStore('user', { /* ... */ })
export const useProductStore = defineStore('products', { /* ... */ })
// Nommage à éviter
export const userStore = defineStore('user', { /* ... */ }) // Préfixe use manquant
2. Initialisation de l'état
Exemple
state: () => ({
items: [],
loading: false,
error: null
})
// Non recommandé : utiliser directement un objet
state: {
items: [] // Cela fera que toutes les instances partageront le même tableau !
}
3. Gestion des opérations asynchrones
Exemple
async fetchUserData(userId) {
this.loading = true
this.error = null
try {
const response = await api.getUser(userId)
this.user = response.data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}
}
4. Persistance des données
Pour les données qui doivent être persistées, vous pouvez utiliser un plugin :
npm install pinia-plugin-persistedstate
Exemple
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: {}
}),
// Activer la persistance
persist: true
})
API Pinia
| Catégorie | API | Description |
|---|---|---|
| Créer | defineStore |
Définir un store indépendant, contenant state / getters / actions |
| Instanciation | createPinia |
Créer une instance racine Pinia pour app.use() |
| Utilisation des composants | useStore()(Défini par l'utilisateur, par ex.useUserStore) |
Appeler un store, renvoie l'état réactif, les getters et les actions |
| State | state |
Renvoie un objet fonction, utilisé pour définir un état global mutable |
| Opérations sur l'état | store.$state |
Lire/écrire directement tout l'état (au niveau de l'objet) |
| Opérations sur l'état | store.$patch() |
Modifier l'état par lots, prend en charge les modes objet et fonction |
| Remplacement de l'état | store.$reset() |
Réinitialiser à l'état initial, disponible uniquement lorsqu'il est défini via la méthode setup |
| Abonnement à l'état | store.$subscribe() |
Écouter les changements d'état, adapté à la persistance locale |
| Invocation d'action | store.$onAction() |
Écouter avant et après les appels d'action, peut être utilisé pour la journalisation et le suivi |
| Getter | getters |
Données dérivées, automatiquement mises en cache en fonction de l'état |
| Plugins | pinia.use() |
Enregistrer des plugins pour étendre les capacités du store |
| Méthode de définition | defineStore(id, options) |
Syntaxe de l'API Options |
| Méthode de définition | defineStore(id, () => {...}) |
Syntaxe setup, renvoie state / getter / action |
| Propriétés du store | store.$id |
Identifiant unique du store actuel |
| Propriétés du store | store.$ready(Certaines versions) |
Indicateur d'état une fois l'initialisation du store terminée |
| Persistance (plugin) | persist |
Activer les plugins de stockage (tels que localStorage) |
| Fonctions utilitaires | storeToRefs() |
Convertir state / getters en refs, en préservant la réactivité |
Autres extensionsPour plus d'informations, consultez le site officiel de Pinia :https://pinia.vuejs.org/