Étape 15 — Système de favoris dans le store
Objectifs
- Ajouter un système de favoris au
pokemonStore(state, actions, getters) - Persister les favoris dans le
localStoragedu navigateur - Comprendre
JSON.stringify()etJSON.parse()pour stocker des données complexes
Prérequis — Point de départ
Cette étape nécessite d'avoir terminé les étapes 1 à 14 (séquences 1-4). Si votre code n'est pas à jour, vous pouvez repartir de la branche etape-15-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-15-start prof/etape-15-start
npm installContexte
On veut permettre à l'utilisateur de marquer des Pokémon comme favoris. Ces favoris doivent survivre au rechargement de la page. Le navigateur offre un espace de stockage local (localStorage) qui persiste même après fermeture du navigateur.
Encart oral — localStorage = string uniquement
Le localStorage ne stocke que des chaînes de caractères. On ne peut pas y mettre directement un tableau ou un objet JavaScript.
// ❌ Ne fonctionne pas comme attendu
localStorage.setItem('favoris', [1, 2, 3])
localStorage.getItem('favoris') // "1,2,3" — une string, pas un tableau !
// ✅ Solution : convertir en JSON
localStorage.setItem('favoris', JSON.stringify([1, 2, 3]))
JSON.parse(localStorage.getItem('favoris')) // [1, 2, 3] — un vrai tableau| Méthode | Direction | Rôle |
|---|---|---|
JSON.stringify() | JS → string | Convertit un objet/tableau en texte JSON |
JSON.parse() | string → JS | Convertit du texte JSON en objet/tableau |
Tâches
1. Ajouter le state favorites
Dans src/stores/pokemonStore.js, ajoutez une propriété favorites au state. C'est un tableau qui contiendra les IDs des Pokémon favoris (pas les objets complets).
state: () => ({
isLoading: false,
types: [],
pokemons: [],
selectedPokemon: null,
favorites: [],
}),Pourquoi stocker des IDs et pas les objets ?
- Les IDs prennent moins de place dans le
localStorage - Les objets Pokémon peuvent changer (niveau, stats...) — l'ID reste stable
- On peut retrouver l'objet complet avec
pokemons.find()
2. Ajouter les getters
Ajoutez trois getters dans la section getters du store :
| Getter | Paramètre | Retour | Rôle |
|---|---|---|---|
totalFavorites | state | number | Nombre de favoris |
isFavorite | state → (pokemon) | boolean | Vérifie si un Pokémon est favori |
getFavorites | state | Array<Object> | Liste des objets Pokémon favoris |
totalFavorites — Simple compteur :
totalFavorites: state => {
return state.favorites.length
},isFavorite — Getter avec paramètre (retourne une fonction) :
isFavorite: state => {
return pokemon => {
return state.favorites.includes(pokemon.id)
}
},getFavorites — Transforme les IDs en objets Pokémon complets :
getFavorites: state => {
const favoritePokemons = state.favorites.map(favoriteId => {
return state.pokemons.find(pokemon => pokemon.id === favoriteId)
})
return favoritePokemons.filter(pokemon => pokemon !== undefined)
},Pourquoi .filter(pokemon => pokemon !== undefined) ?
Si un Pokémon favori a été supprimé de l'API, find() retourne undefined. Le filter() élimine ces cas pour éviter des erreurs dans le template.
3. Ajouter les actions
Ajoutez quatre actions dans la section actions du store :
| Action | Paramètre | Rôle |
|---|---|---|
loadFavorites() | — | Charge les favoris depuis localStorage |
saveFavorites() | — | Sauvegarde les favoris dans localStorage |
toggleFavorite(pokemon) | objet Pokémon | Ajoute ou retire un favori |
cleanupFavorites() | — | Supprime les favoris obsolètes |
loadFavorites — Restaure les favoris au démarrage :
loadFavorites() {
try {
const savedFavorites = localStorage.getItem('pokemon_favorites')
if (savedFavorites) {
this.favorites = JSON.parse(savedFavorites)
console.log('Favoris chargés :', this.favorites.length, 'éléments')
} else {
this.favorites = []
}
} catch (error) {
console.error('Erreur lors du chargement des favoris :', error)
this.favorites = []
}
},saveFavorites — Persiste les favoris :
saveFavorites() {
try {
localStorage.setItem('pokemon_favorites', JSON.stringify(this.favorites))
} catch (error) {
console.error('Erreur lors de la sauvegarde des favoris :', error)
}
},toggleFavorite — Ajoute ou retire un favori (pattern toggle) :
toggleFavorite(pokemon) {
const favoriteIndex = this.favorites.findIndex(
favoriteId => favoriteId === pokemon.id,
)
if (favoriteIndex === -1) {
// Pas encore favori → on l'ajoute
this.favorites.push(pokemon.id)
} else {
// Déjà favori → on le retire
this.favorites.splice(favoriteIndex, 1)
}
// Sauvegarder après chaque changement
this.saveFavorites()
},cleanupFavorites — Nettoie les favoris qui n'existent plus dans l'API :
cleanupFavorites() {
const initialCount = this.favorites.length
this.favorites = this.favorites.filter(favoriteId => {
return this.pokemons.some(pokemon => pokemon.id === favoriteId)
})
const removedCount = initialCount - this.favorites.length
if (removedCount > 0) {
console.log('Nettoyage :', removedCount, 'favoris obsolètes supprimés')
this.saveFavorites()
}
},4. Appeler loadFavorites() dans init()
Dans l'action init() du store, ajoutez l'appel à loadFavorites() après le chargement des Pokémon. L'action init() utilise déjà Axios et Promise.all depuis l'étape 10 :
async init () {
this.isLoading = true
try {
await Promise.all([
this.fetchTypes({ withLoader: false }),
this.fetchPokemons({ withLoader: false }),
])
this.loadFavorites()
console.log('Store initialisé')
} catch (error) {
console.error('Erreur lors de l\'initialisation :', error)
} finally {
this.isLoading = false
}
},Et appelez cleanupFavorites() dans fetchPokemons après le chargement, pour supprimer les favoris périmés :
async fetchPokemons ({ withLoader = true } = {}) {
// ... après this.pokemons = response.data
this.cleanupFavorites()
}Schéma — Flux des favoris
Projet perso — À faire en parallèle
Dans votre projet individuel :
- Identifiez une donnée que l'utilisateur peut mettre en favori ou sélectionner
- Ajoutez un state
favorites(tableau d'IDs) dans votre store - Implémentez
toggleFavorite,loadFavorites,saveFavorites - Persistez avec
localStorage
Références utiles
- MDN — localStorage
- MDN — JSON.stringify()
- MDN — JSON.parse()
- MDN — Array.splice()
- MDN — Array.findIndex()
Tests
- Le state
favoritesexiste dans le store (tableau vide au départ) toggleFavorite()ajoute un ID au tableaufavorites- Appeler
toggleFavorite()deux fois sur le même Pokémon le retire isFavorite()retournetruepour un Pokémon ajoutétotalFavoritescompte correctement les favoris- Après rechargement de la page, les favoris sont restaurés depuis
localStorage - Ouvrir DevTools → Application → Local Storage → la clé
pokemon_favoritesexiste cleanupFavorites()supprime les IDs qui ne correspondent à aucun Pokémon- 0 erreurs dans la console
Solution
src/stores/pokemonStore.js (sections ajoutées)
import { defineStore } from 'pinia'
import api from '@/plugins/axios'
export const usePokemonStore = defineStore('pokemon', {
state: () => ({
isLoading: false,
types: [],
pokemons: [],
favorites: [],
}),
getters: {
totalPokemons: state => {
return state.pokemons.length
},
// Nombre total de favoris
totalFavorites: state => {
return state.favorites.length
},
getTypeById: state => {
return typeId => {
return state.types.find(type => type.id === typeId)
}
},
getPokemonById: state => {
return pokemonId => {
return state.pokemons.find(pokemon => pokemon.id === pokemonId)
}
},
// Vérifie si un Pokémon est favori
isFavorite: state => {
return pokemon => {
return state.favorites.includes(pokemon.id)
}
},
// Retourne les objets Pokémon favoris (pas juste les IDs)
getFavorites: state => {
const favoritePokemons = state.favorites.map(favoriteId => {
return state.pokemons.find(pokemon => pokemon.id === favoriteId)
})
return favoritePokemons.filter(pokemon => pokemon !== undefined)
},
},
actions: {
async init () {
this.isLoading = true
try {
await Promise.all([
this.fetchTypes({ withLoader: false }),
this.fetchPokemons({ withLoader: false }),
])
this.loadFavorites()
console.log('Store initialisé')
} catch (error) {
console.error('Erreur lors de l\'initialisation :', error)
} finally {
this.isLoading = false
}
},
async fetchTypes ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await api.get('/types')
if (response.data && response.data.data) {
this.types = response.data.data
} else if (response.data) {
this.types = response.data
} else {
this.types = []
}
} catch (error) {
console.error('Erreur lors du chargement des types:', error.message)
this.types = []
} finally {
if (withLoader) this.isLoading = false
}
},
async fetchPokemons ({ withLoader = true } = {}) {
if (withLoader) this.isLoading = true
try {
const response = await api.get('/pokemons')
if (response.data && response.data.data) {
this.pokemons = response.data.data
} else if (response.data) {
this.pokemons = response.data
} else {
this.pokemons = []
}
this.cleanupFavorites()
} catch (error) {
console.error('Erreur lors du chargement des Pokémon:', error.message)
this.pokemons = []
} finally {
if (withLoader) this.isLoading = false
}
},
// Charge les favoris depuis localStorage
loadFavorites () {
try {
const savedFavorites = localStorage.getItem('pokemon_favorites')
if (savedFavorites) {
this.favorites = JSON.parse(savedFavorites)
console.log('Favoris chargés :', this.favorites.length, 'éléments')
} else {
this.favorites = []
}
} catch (error) {
console.error('Erreur lors du chargement des favoris :', error)
this.favorites = []
}
},
// Sauvegarde les favoris dans localStorage
saveFavorites () {
try {
localStorage.setItem('pokemon_favorites', JSON.stringify(this.favorites))
} catch (error) {
console.error('Erreur lors de la sauvegarde des favoris :', error)
}
},
// Ajoute ou retire un Pokémon des favoris
toggleFavorite (pokemon) {
const favoriteIndex = this.favorites.findIndex(
favoriteId => favoriteId === pokemon.id,
)
if (favoriteIndex === -1) {
this.favorites.push(pokemon.id)
} else {
this.favorites.splice(favoriteIndex, 1)
}
this.saveFavorites()
},
// Supprime les favoris dont le Pokémon n'existe plus
cleanupFavorites () {
const initialCount = this.favorites.length
this.favorites = this.favorites.filter(favoriteId => {
return this.pokemons.some(pokemon => pokemon.id === favoriteId)
})
const removedCount = initialCount - this.favorites.length
if (removedCount > 0) {
console.log('Nettoyage :', removedCount, 'favoris obsolètes supprimés')
this.saveFavorites()
}
},
},
})Commit
git add -A
git commit -m "feat: système de favoris avec persistance localStorage"