Qu’est-ce que Pinia ?
Pinia est la bibliothèque officielle de gestion d’état pour Vue. Elle a remplacé Vuex en tant que store recommandé pour Vue 3 et est désormais le choix par défaut dans les nouveaux projets Vue et Nuxt. Elle est légère, type-safe et s’appuie directement sur la Composition API.
Sa conception est volontairement minimaliste. Un store est une fonction qui retourne un état, des valeurs dérivées et des actions. Il n’y a pas de mutations, pas de namespacing de modules, ni de provider par store. Si vous connaissez ref, computed et les fonctions, vous connaissez déjà l’essentiel de Pinia.
Définir un store
Un store est créé avec defineStore, qui prend un identifiant unique et une fonction de configuration (setup).
// stores/cart.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCartStore = defineStore("cart", () => {
const items = ref([]);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price, 0),
);
const count = computed(() => items.value.length);
function add(item) {
items.value.push(item);
}
function remove(id) {
items.value = items.value.filter((item) => item.id !== id);
}
return { items, total, count, add, remove };
});
Il s’agit d’un setup store : ref devient l’état (state), computed devient un getter, et les fonctions deviennent des actions. L’identifiant unique permet de nommer le store dans les devtools et est obligatoire.
Pinia supporte également les options stores si vous préférez une structure similaire à Vuex :
// stores/counter.js
export const useCounter = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count += 1;
},
},
});
Les deux formes sont entièrement supportées. Les setup stores ont tendance à mieux s’intégrer avec TypeScript et la Composition API, tandis que les options stores peuvent sembler plus familiers pour les utilisateurs de Vuex.
Utiliser un store dans les composants
Appelez la fonction du store pour obtenir l’instance du store. Il n’y a pas de provider à ajouter autour de vos composants.
<!-- Cart.vue -->
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";
const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }} — {{ item.price }}
<button @click="cart.remove(item.id)">Remove</button>
</li>
</ul>
<p>Total: {{ total }}</p>
</template>
La lecture de cart.total directement dans un template reste réactive. Lorsque vous souhaitez destructurer l’état ou les getters tout en conservant la réactivité, utilisez storeToRefs, qui les convertit en refs. Les actions peuvent être destructurées directement car elles n’ont pas besoin de réactivité.
Getters
Les getters sont des valeurs calculées dérivées de l’état. Ils sont mis en cache et partagés, ainsi chaque composant lisant le même getter partage le même calcul.
// stores/products.js
export const useProductsStore = defineStore("products", () => {
const products = ref([]);
const filter = ref("");
const visible = computed(() =>
products.value.filter((p) =>
p.name.toLowerCase().includes(filter.value.toLowerCase()),
),
);
const inStock = computed(() =>
visible.value.filter((p) => p.stock > 0),
);
return { products, filter, visible, inStock };
});
Tenter d’utiliser watch pour calculer une valeur est une erreur courante. Si une valeur peut être dérivée de l’état existant, elle doit être un getter, exactement comme vous utiliseriez computed dans un composant.
Actions
Les actions sont des fonctions qui modifient l’état. Elles peuvent être synchrones ou asynchrones, et peuvent appeler d’autres actions ou même d’autres stores.
// stores/users.js
export const useUsersStore = defineStore("users", () => {
const users = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
users.value = await res.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
return { users, loading, error, fetchUsers };
});
Cependant, pour les données provenant du serveur, envisagez une couche de récupération de données telle que TanStack Query, qui gère la mise en cache et l’invalidation. Pinia est idéal pour l’état client : paniers, filtres, préférences d’interface utilisateur, état d’authentification et brouillons.
Plugins et persistance
Les plugins Pinia s’exécutent pour chaque store et peuvent ajouter des comportements tels que la persistance, le logging ou la réinitialisation.
// persist.js
export function persistPlugin({ store }) {
const saved = localStorage.getItem(store.$id);
if (saved) store.$patch(JSON.parse(saved));
store.$subscribe((_mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
// main.js
const pinia = createPinia();
pinia.use(persistPlugin);
app.use(pinia);
Le populaire pinia-plugin-persistedstate fait la même chose, avec des options pour choisir les clés à stocker. La persistance est l’une des raisons principales pour lesquelles on privilégie un store plutôt que l’état local d’un composant.
Pinia avec Nuxt
Dans Nuxt, installez le module officiel et les stores situés dans stores/ seront auto-importés.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore(); // auto-imported
</script>
Comme Nuxt gère l’installation et le rendu serveur, le même store fonctionne aussi bien sur le serveur que sur le client. Consultez le guide Nuxt pour comprendre comment cela s’intègre avec useFetch et les routes serveur.
Bonnes pratiques
- Privilégiez les setup stores pour le nouveau code, surtout avec TypeScript.
- Gardez les données dérivées dans des getters plutôt que de surveiller l’état (state).
- Utilisez
storeToRefslors de la déstructuration du state ou des getters. - Conservez les données serveur dans une couche de récupération de données (data-fetching), et non dans Pinia.
- Utilisez des actions pour tout ce qui modifie l’état, y compris les opérations asynchrones.
- Ne persistez que l’état qui doit survivre à un rechargement de la page.
- Nommez vos stores clairement en fonction du domaine qu’ils gèrent.
Erreurs courantes
- Déstructurer le state sans
storeToRefs, ce qui entraîne une perte de réactivité. - Utiliser
watchpour calculer une valeur qui devrait être dérivée via un getter. - Stocker des données récupérées depuis le serveur dans le store et gérer manuellement les indicateurs de chargement.
- Oublier d’installer Pinia, provoquant l’échec des appels au store au moment de l’exécution.
- Créer un seul store énorme au lieu de stores dédiés à des domaines précis.
- Muter le state en dehors d’une action, rendant le traçage des modifications difficile.
Et après ?
Pinia est l’équivalent Vue d’un store moderne et léger. Approfondissez vos connaissances en Vue, ajoutez Nuxt pour le rendu serveur et les auto-imports, et comparez cette approche avec la Context API, Zustand et Redux Toolkit de React. Ensuite, créez un petit store avec un getter, une action asynchrone et de la persistance pour constater à quel point cela demande peu de code.