Pinia en bref¶
Pinia = le "cerveau central" de votre application Vue
Pinia est une bibliothèque de stockage et/ou un gestionnaire d'état pour Vue.js. Il permet de partager un état entre les composants ou les views (pages) de l'application par l'intermédiaire d'une zone de stockage partagée appelé store.
Le partage de données entre un composant parent et enfant peut-être réalisé classiquement via des propset emit. Cependant, si nous souhaitons partager un état entre de nombreuses pages/composants, cela devient un peu complexe à gérer.
Voilà pourquoi Pinia existe!
C'est un endroit où vous pouvez stocker des données qui devront être partagées entre plusieurs composants Vue.
Pourquoi Pinia, quel problème résout-il?
🤔Le problème qu'il résout¶
Sans Pinia (le cauchemar)¶
Imaginez que vous avez:
- Un composant
Header.vuequi affiche le nom de l'utilisateur - Un composant
Sidebar.vuequi liste les salles du musée - Un composant
MemoryList.vuequi affiche les mémoires - Un composant
AddMemoryForm.vuequi ajoute une mémoire
Comment faire circuler les données entre tous ces composants?
App.vue (parent)
├── Header.vue (affiche userName)
├── Sidebar.vue (affiche rooms)
└── MainContent.vue
├── MemoryList.vue (affiche memoryList (la liste des souvenirs))
└── AddMemoryForm.vue (ajoute une memory (un souvenir))
Sans Pinia, vous devez:
- Passer les données de parent en enfant avec
props(fastidieux!) - Remonter les événements avec
emits(complexe!) - Dupliquer les données dans plusieurs composants (cauchemar de synchronisation!)
Exemple sans Pinia (props hell):
<!-- App.vue -->
<template>
<Header :userName="userName" />
<Sidebar :rooms="rooms" @room-added="addRoom" />
<MainContent
:rooms="rooms"
:memories="memories"
@memory-added="addMemory"
/>
</template>
<script>
export default {
data() {
return {
userName: 'Alice',
rooms: [...],
memories: [...]
};
},
methods: {
addRoom(room) { /* ... */ },
addMemory(memory) { /* ... */ }
}
}
</script>
Vous devez passer TOUT contenu à travers les props, même aux composants profondément imbriqués! 😱
Avec Pinia (la solution élégante)¶
Vous créez un "store" (magasin) central où TOUS les composants peuvent:
- Lire les données directement
- Modifier les données directement
- S'abonner aux changements automatiquement
<!-- Dans n'importe quel composant, n'importe où -->
<script setup>
/* On importe la méthode use...Store depuis le store
qu'on aura préalablement créé */
import { useMuseumStore } from '@/stores/museumStore';
// On stock la méthode dans une constante interne
const museumStore = useMuseumStore();
// Lire des données du store
console.log(museumStore.rooms);
// Ajouter une données au store (ici on ajoute une mémoire)
museumStore.addMemory(roomId, memoryData);
</script>
Magique! Tous les composants qui utilisent museumStore se mettent à jour automatiquement. ✨
🔄Comparaison: Composant vs Store¶
| Composant Vue | Store Pinia |
|---|---|
| data() | state() |
| computed | getters |
| methods | actions |
| Local à un composant | Global à toute l'app |
C'est comme un composant Vue, mais partagé partout!¶
Installation de Pinia¶
Vérifiez si vous ne l'avez pas déjà installé avec le package Vite. Pour ce faire, ouvrez le ficheir package.json et vérifiez si "pinia" fait partie de la liste des "dependencies".
Si Pinia n'est pas déjà installé, vous pouvez l'installer en entrant cette commande dans votre terminal
npm install pinia
Initialisation de Pinia¶
Pour initialiser Pinia, vous devez importer la méthode createApp() dans votre fichier main.js puis l'enregistrer avec app.use().
src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// ...
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Définir un store Pinia¶
Un store Pinia a 3 parties principales:
STATE¶
STATE: Les données (comme data() dans un composant classique Vue)
GETTERS¶
GETTERS: Données calculées (comme les propriétés calculées computed dans un composant classique Vue)
Dans un getter, si on veut accéder au données state, on doit écrire préfixer le nom de la données par state..
Par exemple state.dataName où dataName est le nom de la données à laquelle on se réfère.
ACTIONS¶
ACTIONS: Fonctions qui modifient le state (comme methods dans un composant classique Vue)
Dans un action, si on veut accéder au données state, on doit écrire préfixer le nom de la données par this..
Par exemple this.dataName où dataName est le nom de la données à laquelle on se réfère.
Définir un store Pinia: le fichier .js¶
Pour définir un nouveau store Pinia, il faut créer un nouveau fichier JavaScript. Ces fichiers JavaScript doivent être placés dans le un dossier stores dans src juste à coté des dossiers components, router, views etc.

Par exemple un store Pinia qui s'appellerait museumStore serait défini dans un fichier placé ici:
src/stores/museum.js
// On importe la méthode defineStore depuis le module `pinia`
import { defineStore } from 'pinia';
// On défnit un store appelé "museum"
// (ou autre nom adapté à votre projet)
export const useMuseumStore = defineStore('museum', {
/*
1️⃣ STATE - Les données
(comme data() dans un composant classique Vue)
*/
state: () => ({
rooms: [],
currentRoomId: null,
userName: 'Alice'
}),
/*
2️⃣ GETTERS - Données calculées
(comme computed dans un composant classique Vue)
*/
getters: {
currentRoom: (state) => {
/* find() recherche dans l'array des salles (state.rooms)
la salle courante (state.currentRoomId) */
return state.rooms.find(room => room.id === state.currentRoomId);
},
totalMemories: (state) => {
/* reduce() additionne le nombre de memories accumulées jusqu'à présent
dans toutes les salles au nombre de memories de la salle actuelle */
return state.rooms.reduce((sum, room) =>
sum + room.memories.length, 0
);
}
},
/*
3️⃣ ACTIONS - Fonctions qui modifient le state
(comme methods dans un composant classique Vue)
*/
actions: {
addRoom(room) {
this.rooms.push(room);
},
deleteRoom(roomId) {
// On cherche l'INDEX (la position) de la room dans l'array des salles
const index = this.rooms.findIndex(r => r.id === roomId);
// On le retire du tableau des salles
this.rooms.splice(index, 1);
}
}
});
Accéder au stores¶
Pour accéder aux éléments d'un store dans un composant Vue, il faut d'abord importer ce fameux store.
Ensuite, on fait appel à la fonction de pinia nommée mapStore() afin de mapper l'ensemble des éléments du store (state, getters) au sein de propriétés calculées computed du composant.
import { mapStores } from 'pinia'
import { useMuseumStore } from '../stores/museum'
export default {
computed: {
// Store accessible via l'objet this.useMuseumStore
...mapStores(useMuseumStore),
}
}
Ensuite, ces propriétés calculées sont accessibles via un objet nommé: identifiant du store + Store. Par exemple, ici ce serait museumStore.
Par exemple:
museumStore.rooms // Pour le state qui contient la liste des salles
museumStore.currentRoom // Pour un getter qui retourne le contenu de la salle courante
museumStore.deleteRoom(4) // Pour une action qui supprime la salle ayant l'id 4
Exemple complet d'un compteur utilisant un store Pinia¶
Si vous souhaitez explorer cet exemple, voici les fichiers de cet exemple de compteur utilisant Pinia à télécharger et installer localement (n'oubliez pas le npm install et le npm run dev).
📥 Charger les fichiers du projet du compteur utilisant Pinia
On enregistre les composants componentA.vue et componentV.vue
Fichier src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import ComponentA from './components/ComponentA.vue'
import ComponentB from './components/ComponentB.vue'
import './assets/main.css'
const app = createApp(App)
app.use(createPinia())
app.component('ComponentA', ComponentA)
app.component('ComponentB', ComponentB)
app.mount('#app')
On utilise ces 2 composants dans App.vue
Fichier src/App.vue
<template>
<ComponentA/>
<ComponentB/>
</template>
On déclare un store counter.js
Fichier src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 0
}),
actions: {
increment() {
this.counter++
}
},
getters: {
isEven: (state) => {
return state.counter % 2 == 0
}
}
})
On peut désormais utiliser ce store partagé dans plusieurs composants.
Par exemple, j'incrémente le compteur du store depuis ComponentA
Fichier src/components/ComponentA.vue
<template>
<h1>Component A</h1>
<button @click="counterStore.increment()">Ajouter</button>
</template>
<script>
import { useCounterStore } from '../stores/counter'
import { mapStores } from 'pinia'
export default {
computed: {
...mapStores(useCounterStore),
}
}
</script>
Je récupère la valeur du compteur du store depuis ComponentB:
Fichier src/components/ComponentB.vue
<template>
<h1>Component B</h1>
<p>{{ counterStore.counter }}</p>
</template>
<script>
import { useCounterStore } from '../stores/counter'
import { mapStores } from 'pinia'
export default {
computed: {
...mapStores(useCounterStore),
}
}
</script>
Configuration de stores pour le projet App web créative¶
Pour Mémoires interactives¶
Structure des stores suggérée:
-
museum- State (équivalent de data()):
roomscurrentRoomIdmuseumNametheme
- Actions (équivalent de methods):
addRoom()(optionnel car certains projets ne le permettent pas)updateRoom()deleteRoom()setCurrentRoom()
- State (équivalent de data()):
-
memory- State (équivalent de data()):
memoriesfilterssearchQuery
- Actions (équivalent de methods):
addMemory()updateMemory()deleteMemory()searchMemories()
- Getters (équilavent de computed):
filteredMemoriesmemoriesByRoommemoriesByTag
- State (équivalent de data()):
-
auth(optionnel)- State (équivalent de data()):
userisAuthenticated
- Actions (équivalent de methods):
login()logout()register()
- State (équivalent de data()):
Checklist Mémoires interactives¶
- Création des 2 stores obligatoires:
-
museum.js(structure de base) -
memory.js(structure de base)
-
- Développement des composants clés qui utilisent les stores:
-
RoomCard.vue(carte de salle) -
MemoryCard.vue(carte de mémoire) -
MemoryList.vue(grille de mémoires)
-
Pour Trace ton chemin¶
Structure des stores suggérée:
-
story(le plus important du projet)- State (équivalent de data()):
currentChapterIdvisitedChaptersstoryDataavailableChoices
- Actions (équivalent de methods):
loadChapter()makeChoice()goToChapter()
- Getters (équilavent de computed):
currentChapterisChapterUnlocked()
- State (équivalent de data()):
-
player(pour le système de conséquences)- State (équivalent de data()):
playerName(le nom du joueur)karmastatsinventoryflagsrelationships
- Actions (équivalent de methods):
addToInventory()updateStat()setFlag()updateRelationship()
- Getters (équilavent de computed):
hasItem()getRelationship()canAccessEnding()
- State (équivalent de data()):
-
save- State (équivalent de data()):
saveSlots(array de 3 slots)
- Actions (équivalent de methods):
saveGame()loadGame()deleteSave()getSaveInfo()
- Getters (équilavent de computed):
hasSaveslatestSave
- State (équivalent de data()):
-
useAudioStore(optionnel)- State (équivalent de data()):
currentMusicsoundEffectsvolumeisMuted
- Actions (équivalent de methods):
playMusic()playSound()toggleMute()setVolume()
- State (équivalent de data()):
Checklist Trace ton chemin¶
- Création des 2 premier stores:
-
story.js(chapitres, navigation) -
player.js(état du joueur, commencez réalistement, juste avec son nom)
-
- Création du fichier JSON avec les chapitres
- Développement des composants clés qui utilisent les stores:
-
ChoiceButton.vue(bouton de choix) -
ChoicePanel.vue(panel de choix)
-
Template générique d'un Store Pinia¶
Utilisez ce template générique de base pour débuter:
import { defineStore } from 'pinia';
export const useExampleStore = defineStore('example', {
state: () => ({
items: [],
currentItem: null,
isLoading: false,
error: null
}),
getters: {
// Retourne le nombre d'items dansle array state.items
itemCount: (state) => state.items.length,
/* Retourne true si l'array state.items contient des items
et false s'il est vide */
hasItems: (state) => state.items.length > 0,
/* Récupére un item spécifique de l'array state.items
par son id */
getItemById: (state) => (id) => {
return state.items.find(item => item.id === id);
},
currentChapter: (state) => {
return state.items[state.currentItem];
},
},
actions: {
// ajoute un items au tableau this.items
addItem(item) {
this.items.push({
...item,
id: Date.now().toString(),
createdAt: new Date().toISOString()
});
},
/* modifie un item spécifique du tableau this.items
en lui spécifiant son id */
updateItem(id, updates) {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items[index] = { ...this.items[index], ...updates };
}
},
/* supprime un item du tableau this.items
en lui spécifiant son id */
deleteItem(id) {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
}
},
/* modifie l'item actuel à afficher (storé dans this.currentItem)
en lui spécifiant le id */
setCurrentItem(id) {
this.currentItem = this.getItemById(id);
}
}
});
Exemple d'un composant intégrant un store Pinia¶
Ajout du 13 novembre 2025
<template>
<div class="items-list">
<h1>Liste des items ({{ exampleStore.itemCount }})</h1>
<div v-if="exampleStore.isLoading">Chargement...</div>
<div v-else-if="exampleStore.hasItems">
<div
v-for="item in exampleStore.items"
:key="item.id"
class="item-card"
>
<h3>{{ item.name }}</h3>
<button @click="selectItem(item.id)">Voir</button>
<button @click="removeItem(item.id)">Supprimer</button>
</div>
</div>
<div v-else>
<p>Aucun item</p>
</div>
<ButtonPrimary @click="addNewItem">
Ajouter un item
</ButtonPrimary>
</div>
</template>
<script>
import { useExampleStore } from '@/stores/exampleStore';
import { mapStores } from 'pinia';
import ButtonPrimary from '@/components/ui/ButtonPrimary.vue';
export default {
name: 'ItemsList',
components: {
ButtonPrimary
},
computed: {
// Mapper le store complet
// Cela donne accès à : exampleStore.state, exampleStore.getters, exampleStore.actions
...mapStores(useExampleStore)
},
methods: {
addNewItem() {
// Accès aux actions via exampleStore
this.exampleStore.addItem({
name: `Item ${this.exampleStore.itemCount + 1}`,
description: 'Nouvel item'
});
},
removeItem(id) {
if (confirm('Supprimer cet item?')) {
// Accès aux actions via exampleStore
this.exampleStore.deleteItem(id);
}
},
selectItem(id) {
// Accès aux actions via exampleStore
this.exampleStore.setCurrentItem(id);
this.$router.push(`/item/${id}`);
}
}
};
</script>
<style scoped>
.items-list {
padding: 2rem;
}
.item-card {
border: 1px solid #ddd;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
</style>