Étape 6 — Créer le store Pokémon
Objectifs
- Comprendre le problème du prop drilling et pourquoi un store est nécessaire
- Créer un store Pinia avec
defineStore()(syntaxe Options) - Définir le state, les getters et les actions
- Déplacer les appels
fetch()dans le store
Prérequis — Point de départ
Cette étape nécessite d'avoir terminé les étapes 1 à 5 (séquence 1). Si votre code n'est pas à jour, vous pouvez repartir de la branche etape-6-start :
# Ajouter le dépôt du prof (une seule fois)
git remote add prof https://github.com/fallinov/esig-141-pokedex-vuetify.git
# Récupérer les branches et basculer
git fetch prof
git checkout -b etape-6-start prof/etape-6-start
npm installContexte — Le problème
À la fin de la séquence 1, vous avez un problème : fetch() est appelé deux fois (dans index.vue et dans [id].vue). Si on ajoute des favoris ou des filtres, chaque page devrait re-télécharger les données. C'est du gaspillage.
La solution : centraliser les données dans un store Pinia. Le store charge les données une seule fois, et toutes les pages y accèdent.
Encart oral — State, Getters, Actions
Un store Pinia est composé de trois parties :
| Partie | Rôle | Analogie |
|---|---|---|
| state | Les données brutes | Les tiroirs d'une armoire |
| getters | Données calculées (lecture seule) | Un computed dans un composant |
| actions | Fonctions qui modifient le state | Les méthodes qui ouvrent les tiroirs |
Le state est la seule source de vérité de l'application. Les composants lisent le state (via les getters ou directement) et appellent les actions pour le modifier.
Tâches
1. Créer le fichier pokemonStore.js
Créez le fichier src/stores/pokemonStore.js. La structure du store suit la syntaxe Options de Pinia :
import { defineStore } from 'pinia'
export const usePokemonStore = defineStore('pokemon', {
state: () => ({
// données ici
}),
getters: {
// données calculées ici
},
actions: {
// fonctions ici
},
})Convention de nommage
Le nom du composable commence toujours par use et se termine par Store : usePokemonStore. C'est une convention Pinia que vous devez respecter dans votre projet perso.
2. Définir le state
Le state contient les données partagées de l'application :
| Propriété | Type | Valeur initiale | Rôle |
|---|---|---|---|
pokemons | Array | [] | Liste de tous les Pokémon |
types | Array | [] | Types de Pokémon (Feu, Eau, etc.) |
isLoading | Boolean | false | Indicateur de chargement |
error | String|null | null | Message d'erreur éventuel |
3. Définir les getters
Les getters sont des propriétés calculées qui dépendent du state :
| Getter | Paramètre | Retour | Rôle |
|---|---|---|---|
totalPokemons | — | number | Nombre total de Pokémon |
getPokemonById | id (string) | Object|undefined | Trouve un Pokémon par son ID |
Encart oral — Getter avec paramètre
Un getter sans paramètre s'écrit simplement :
totalPokemons: (state) => state.pokemons.lengthUn getter avec paramètre retourne une fonction :
getPokemonById: (state) => {
return (pokemonId) => {
return state.pokemons.find(p => p.id === pokemonId)
}
}Dans le composant, on l'utilise comme une fonction : pokemonStore.getPokemonById(id).
4. Définir les actions
Les actions sont des méthodes asynchrones (ou non) qui modifient le state :
fetchPokemons()
- Appeler
fetch('http://localhost:3535/pokemons') - Vérifier que la réponse est OK (
response.ok) - Convertir la réponse en JSON
- Stocker le résultat dans
this.pokemons - Gérer les erreurs avec
try/catch(stocker le message dansthis.error)
fetchTypes()
Même logique, mais pour http://localhost:3535/types. Stocker dans this.types.
isLoading — une seule responsabilité
fetchPokemons() et fetchTypes() ne gèrent pas isLoading. C'est init() qui s'en charge, car c'est elle qui orchestre le chargement global. Si chaque action gérait isLoading individuellement, isLoading passerait à false dès que la première requête termine — alors que la deuxième est encore en cours.
init()
Action d'initialisation qui appelle les deux fetch en parallèle avec Promise.all :
async init() {
this.isLoading = true
try {
await Promise.all([
this.fetchPokemons(),
this.fetchTypes(),
])
} catch (error) {
this.error = 'Erreur lors du chargement des données'
console.error(error)
} finally {
this.isLoading = false
}
}Attention au this
Dans les actions Pinia (syntaxe Options), on accède au state avec this :
this.pokemons = data // ✅ Correct
this.isLoading = true // ✅ CorrectC'est différent de la Composition API où on utilise .value.
Projet perso — À faire en parallèle
Dans votre projet individuel :
- Créez
src/stores/votreStore.js(ex :useRecipeStore,useMovieStore) - Définissez le state avec vos données (liste d'items, types/catégories, isLoading)
- Écrivez les actions
fetchItems()etinit() - Ajoutez un getter
getItemById
Tests
- Le fichier
src/stores/pokemonStore.jsexiste - Le store exporte
usePokemonStore - Le state contient
pokemons,types,isLoading,error - Le getter
totalPokemonsretourne le nombre de Pokémon - Le getter
getPokemonByIdretrouve un Pokémon par son ID - L'action
fetchPokemons()charge les données depuis l'API - L'action
fetchTypes()charge les types depuis l'API - L'action
init()appelle les deux en parallèle et gèreisLoading - Les erreurs sont gérées avec
try/catchdansinit()
Solution
src/stores/pokemonStore.js
import { defineStore } from 'pinia'
/**
* Store Pinia pour gérer les données des Pokémon.
* Centralise les appels API et partage les données entre les pages.
*/
export const usePokemonStore = defineStore('pokemon', {
/**
* State — les données brutes du store.
* Retourne une fonction qui retourne un objet (comme data() dans Options API).
*/
state: () => ({
// Liste de tous les Pokémon chargés depuis l'API
pokemons: [],
// Liste des types de Pokémon (Feu, Eau, Plante, etc.)
types: [],
// Indicateur de chargement — true pendant les appels API
isLoading: false,
// Message d'erreur en cas de problème
error: null,
}),
/**
* Getters — propriétés calculées basées sur le state.
* Équivalent de computed() dans un composant.
*/
getters: {
/**
* Nombre total de Pokémon dans le store.
* @param {Object} state - Le state du store
* @returns {number}
*/
totalPokemons: (state) => {
return state.pokemons.length
},
/**
* Trouve un Pokémon par son identifiant.
* Retourne une fonction (getter avec paramètre).
* @param {Object} state - Le state du store
* @returns {function(string): Object|undefined}
*/
getPokemonById: (state) => {
return (pokemonId) => {
return state.pokemons.find(pokemon => pokemon.id === pokemonId)
}
},
},
/**
* Actions — méthodes qui modifient le state.
* Peuvent être asynchrones (appels API).
* On accède au state avec `this`.
*/
actions: {
/**
* Charge tous les Pokémon depuis l'API.
* Note : ne gère pas isLoading — c'est init() qui s'en charge.
*/
async fetchPokemons() {
const response = await fetch('http://localhost:3535/pokemons')
// Vérifier que la réponse est OK (status 200-299)
if (!response.ok) {
throw new Error(`Erreur HTTP : ${response.status}`)
}
this.pokemons = await response.json()
console.log('Pokémon chargés :', this.pokemons.length)
},
/**
* Charge tous les types de Pokémon depuis l'API.
* Note : ne gère pas isLoading — c'est init() qui s'en charge.
*/
async fetchTypes() {
const response = await fetch('http://localhost:3535/types')
if (!response.ok) {
throw new Error(`Erreur HTTP : ${response.status}`)
}
this.types = await response.json()
console.log('Types chargés :', this.types.length)
},
/**
* Initialise le store : charge les Pokémon et les types en parallèle.
* À appeler une seule fois au démarrage de l'application (dans App.vue).
*/
async init() {
console.log('Initialisation du store Pokémon...')
this.isLoading = true
this.error = null
try {
// Promise.all exécute les deux requêtes en parallèle
// Plus rapide que de les faire l'une après l'autre
await Promise.all([
this.fetchPokemons(),
this.fetchTypes(),
])
console.log('Store Pokémon initialisé')
} catch (error) {
this.error = 'Erreur lors du chargement des données'
console.error(error)
} finally {
this.isLoading = false
}
},
},
})Commit
git add -A
git commit -m "feat: store Pinia pokemonStore avec fetch"