Skip to content

Étape 7 — Connecter les pages au store

Objectifs

  • Appeler pokemonStore.init() dans App.vue pour charger les données une seule fois
  • Remplacer les fetch() locaux par le store dans index.vue et [id].vue
  • Comprendre storeToRefs() et pourquoi c'est nécessaire
  • Utiliser un getter avec paramètre (getPokemonById)

Contexte

Le store est créé, mais aucune page ne l'utilise encore. Actuellement :

  • index.vue fait son propre fetch() dans onMounted
  • [id].vue fait aussi son propre fetch() dans onMounted

On va remplacer tout ça par le store. Le chargement se fera une seule fois dans App.vue, et les pages liront simplement les données du store.

Tâches

1. Initialiser le store dans App.vue

App.vue est le composant racine — il est monté une seule fois au démarrage. C'est l'endroit idéal pour initialiser le store.

Modifiez App.vue pour :

  1. Importer usePokemonStore depuis @/stores/pokemonStore
  2. Dans onMounted, appeler pokemonStore.init()
js
import { usePokemonStore } from '@/stores/pokemonStore'

onMounted(async () => {
  const pokemonStore = usePokemonStore()
  await pokemonStore.init()
})

Pourquoi dans App.vue et pas dans main.js ?

On pourrait techniquement appeler init() dans main.js, mais onMounted dans App.vue est plus explicite : on voit clairement quand les données sont chargées dans le cycle de vie de l'application.

2. Modifier index.vue — Utiliser le store

Remplacez tout le code de chargement (ref, onMounted, fetch) par le store :

Avant (séquence 1) :

js
const pokemons = ref([])

onMounted(async () => {
  const response = await fetch('http://localhost:3535/pokemons')
  pokemons.value = await response.json()
})

Après (séquence 2) :

js
import { usePokemonStore } from '@/stores/pokemonStore'
import { storeToRefs } from 'pinia'

const pokemonStore = usePokemonStore()
const { pokemons } = storeToRefs(pokemonStore)

Le onMounted et le ref disparaissent de index.vue. Plus besoin : c'est App.vue qui charge les données.

Encart oral — storeToRefs : pourquoi c'est nécessaire

Quand on destructure un store Pinia, les propriétés perdent leur réactivité :

js
// ❌ MAUVAIS — pokemons n'est plus réactif
const { pokemons } = pokemonStore

storeToRefs() convertit chaque propriété du state en ref, ce qui préserve la réactivité :

js
// ✅ BON — pokemons reste réactif
const { pokemons } = storeToRefs(pokemonStore)

Règle simple : utiliser storeToRefs() pour le state et les getters. Les actions se destructurent directement car ce sont des fonctions (pas des données réactives).

js
// State et getters → storeToRefs
const { pokemons, types, isLoading } = storeToRefs(pokemonStore)

// Actions → destructuration directe
const { init, fetchPokemons } = pokemonStore

Dans le template, remplacez pokemons par la variable destructurée :

vue
<v-col
  v-for="pokemon in pokemons"
  :key="pokemon.id"
  cols="12" sm="6" md="4" lg="3"
>
  <pokemon-card :pokemon="pokemon" />
</v-col>

Alternative sans storeToRefs

Vous pouvez aussi accéder aux données directement via le store sans destructurer :

vue
<v-col v-for="pokemon in pokemonStore.pokemons" ...>

Dans ce cas, pas besoin de storeToRefs. C'est plus simple mais plus verbeux dans le template. Les deux approches sont valides.

3. Modifier [id].vue — Utiliser le getter

Remplacez le fetch() local par le getter getPokemonById du store :

Avant (séquence 1) :

js
const route = useRoute()
const pokemons = ref([])
const pokemon = computed(() => {
  return pokemons.value.find(p => p.id === route.params.id)
})

onMounted(async () => {
  const response = await fetch('http://localhost:3535/pokemons')
  pokemons.value = await response.json()
})

Après (séquence 2) :

js
import { usePokemonStore } from '@/stores/pokemonStore'

const route = useRoute()
const pokemonStore = usePokemonStore()

const pokemon = computed(() => {
  return pokemonStore.getPokemonById(route.params.id)
})

Le ref, le onMounted et le fetch disparaissent. Le computed utilise le getter du store qui cherche dans les données déjà chargées.

Le double fetch() est résolu !

Rappelez-vous le warning de l'étape 4 : on appelait fetch() deux fois (une dans index.vue, une dans [id].vue). Maintenant, App.vue appelle init() une seule fois et les deux pages lisent le même store. Problème résolu.

Projet perso — À faire en parallèle

Dans votre projet individuel :

  1. Initialisez votre store dans App.vue avec onMounted
  2. Remplacez les fetch() locaux par le store dans votre page liste
  3. Utilisez un getter pour la page de détail
  4. Testez que storeToRefs() fonctionne (ou accédez directement via le store)

Tests

  • App.vue appelle pokemonStore.init() dans onMounted
  • index.vue n'a plus de fetch() ni de onMounted
  • index.vue utilise storeToRefs() (ou pokemonStore.pokemons directement)
  • La grille de Pokémon s'affiche comme avant
  • [id].vue n'a plus de fetch() ni de onMounted
  • [id].vue utilise pokemonStore.getPokemonById()
  • La page de détail affiche le bon Pokémon
  • Navigation accueil → détail → retour fonctionne
  • 0 erreurs dans la console
  • L'onglet Pinia dans Vue DevTools montre les données du store

Solutions

src/App.vue
vue
<template>
  <v-app>
    <app-header />
    <v-main>
      <router-view />
    </v-main>
    <app-footer />
  </v-app>
</template>

<script setup>
// Imports des composants de layout
import AppHeader from '@/components/AppHeader.vue'
import AppFooter from '@/components/AppFooter.vue'

// Import du store Pokémon
import { usePokemonStore } from '@/stores/pokemonStore'

// Initialisation du store au montage de l'application
// onMounted est auto-importé grâce à unplugin-auto-import
onMounted(async () => {
  const pokemonStore = usePokemonStore()
  // init() charge les Pokémon et les types en parallèle
  await pokemonStore.init()
})
</script>
src/pages/index.vue
vue
<template>
  <v-container>
    <h1 class="text-h3 text-center my-6">Pokédex</h1>

    <v-row>
      <v-col
        v-for="pokemon in pokemons"
        :key="pokemon.id"
        cols="12"
        sm="6"
        md="4"
        lg="3"
      >
        <pokemon-card :pokemon="pokemon" />
      </v-col>
    </v-row>
  </v-container>
</template>

<script setup>
// Import du store et de storeToRefs
import { usePokemonStore } from '@/stores/pokemonStore'
import { storeToRefs } from 'pinia'
import PokemonCard from '@/components/PokemonCard.vue'

// Instancier le store
const pokemonStore = usePokemonStore()

// Destructurer le state en gardant la réactivité
// storeToRefs convertit chaque propriété du state en ref
const { pokemons } = storeToRefs(pokemonStore) 
</script>
src/pages/pokemon/[id].vue
vue
<template>
  <v-container>
    <v-btn
      variant="text"
      prepend-icon="mdi-arrow-left"
      class="mb-4"
      @click="$router.back()"
    >
      Retour
    </v-btn>

    <v-alert
      v-if="!pokemon"
      type="error"
      variant="tonal"
    >
      Pokémon non trouvé.
    </v-alert>

    <v-card
      v-else
      max-width="800"
      class="mx-auto"
    >
      <v-img
        :src="getImageUrl(pokemon.img)"
        :alt="pokemon.name"
        height="300"
        cover
      />

      <v-card-title class="text-h4">
        {{ pokemon.name }}
      </v-card-title>

      <v-card-subtitle>
        Niveau {{ pokemon.level }}
      </v-card-subtitle>

      <v-card-text>
        <p v-if="pokemon.description" class="text-body-1 mb-4">
          {{ pokemon.description }}
        </p>
      </v-card-text>
    </v-card>
  </v-container>
</template>

<script setup>
import { getImageUrl } from '@/utils/imageUrl'
// Import du store Pokémon
import { usePokemonStore } from '@/stores/pokemonStore'

// Récupérer l'ID depuis les paramètres de la route
const route = useRoute()

// Instancier le store
const pokemonStore = usePokemonStore()

// Utiliser le getter du store pour trouver le Pokémon
// computed se met à jour automatiquement si l'ID change
const pokemon = computed(() => { 
  return pokemonStore.getPokemonById(route.params.id)
})
</script>

Commit

bash
git add -A
git commit -m "feat: connexion des pages au store Pinia"

Documentation pour les cours de développement web