Skip to content

É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 localStorage du navigateur
  • Comprendre JSON.stringify() et JSON.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 :

bash
# 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 install

Voir la branche sur GitHub

Contexte

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.

js
// ❌ 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éthodeDirectionRôle
JSON.stringify()JS → stringConvertit un objet/tableau en texte JSON
JSON.parse()string → JSConvertit 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).

js
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 :

GetterParamètreRetourRôle
totalFavoritesstatenumberNombre de favoris
isFavoritestate(pokemon)booleanVérifie si un Pokémon est favori
getFavoritesstateArray<Object>Liste des objets Pokémon favoris

totalFavorites — Simple compteur :

js
totalFavorites: state => {
  return state.favorites.length
},

isFavorite — Getter avec paramètre (retourne une fonction) :

js
isFavorite: state => {
  return pokemon => {
    return state.favorites.includes(pokemon.id)
  }
},

getFavorites — Transforme les IDs en objets Pokémon complets :

js
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 :

ActionParamètreRôle
loadFavorites()Charge les favoris depuis localStorage
saveFavorites()Sauvegarde les favoris dans localStorage
toggleFavorite(pokemon)objet PokémonAjoute ou retire un favori
cleanupFavorites()Supprime les favoris obsolètes

loadFavorites — Restaure les favoris au démarrage :

js
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 :

js
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) :

js
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 :

js
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 :

js
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 :

js
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 :

  1. Identifiez une donnée que l'utilisateur peut mettre en favori ou sélectionner
  2. Ajoutez un state favorites (tableau d'IDs) dans votre store
  3. Implémentez toggleFavorite, loadFavorites, saveFavorites
  4. Persistez avec localStorage

Références utiles

Tests

  • Le state favorites existe dans le store (tableau vide au départ)
  • toggleFavorite() ajoute un ID au tableau favorites
  • Appeler toggleFavorite() deux fois sur le même Pokémon le retire
  • isFavorite() retourne true pour un Pokémon ajouté
  • totalFavorites compte correctement les favoris
  • Après rechargement de la page, les favoris sont restaurés depuis localStorage
  • Ouvrir DevTools → Application → Local Storage → la clé pokemon_favorites existe
  • cleanupFavorites() supprime les IDs qui ne correspondent à aucun Pokémon
  • 0 erreurs dans la console

Solution

src/stores/pokemonStore.js (sections ajoutées)
js
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

bash
git add -A
git commit -m "feat: système de favoris avec persistance localStorage"

Documentation pour les cours de développement web