Skip to content

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

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-6-start prof/etape-6-start
npm install

Voir la branche sur GitHub

Contexte — 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 :

PartieRôleAnalogie
stateLes données brutesLes tiroirs d'une armoire
gettersDonnées calculées (lecture seule)Un computed dans un composant
actionsFonctions qui modifient le stateLes 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 :

js
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éTypeValeur initialeRôle
pokemonsArray[]Liste de tous les Pokémon
typesArray[]Types de Pokémon (Feu, Eau, etc.)
isLoadingBooleanfalseIndicateur de chargement
errorString|nullnullMessage d'erreur éventuel

3. Définir les getters

Les getters sont des propriétés calculées qui dépendent du state :

GetterParamètreRetourRôle
totalPokemonsnumberNombre total de Pokémon
getPokemonByIdid (string)Object|undefinedTrouve un Pokémon par son ID

Encart oral — Getter avec paramètre

Un getter sans paramètre s'écrit simplement :

js
totalPokemons: (state) => state.pokemons.length

Un getter avec paramètre retourne une fonction :

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

  1. Appeler fetch('http://localhost:3535/pokemons')
  2. Vérifier que la réponse est OK (response.ok)
  3. Convertir la réponse en JSON
  4. Stocker le résultat dans this.pokemons
  5. Gérer les erreurs avec try/catch (stocker le message dans this.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 :

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

js
this.pokemons = data   // ✅ Correct
this.isLoading = true  // ✅ Correct

C'est différent de la Composition API où on utilise .value.

Projet perso — À faire en parallèle

Dans votre projet individuel :

  1. Créez src/stores/votreStore.js (ex : useRecipeStore, useMovieStore)
  2. Définissez le state avec vos données (liste d'items, types/catégories, isLoading)
  3. Écrivez les actions fetchItems() et init()
  4. Ajoutez un getter getItemById

Tests

  • Le fichier src/stores/pokemonStore.js existe
  • Le store exporte usePokemonStore
  • Le state contient pokemons, types, isLoading, error
  • Le getter totalPokemons retourne le nombre de Pokémon
  • Le getter getPokemonById retrouve 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ère isLoading
  • Les erreurs sont gérées avec try/catch dans init()

Solution

src/stores/pokemonStore.js
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

bash
git add -A
git commit -m "feat: store Pinia pokemonStore avec fetch"

Références

Documentation pour les cours de développement web