Skip to content

Étape 10 — Migrer fetch vers Axios + loading et erreurs

Objectifs

  • Remplacer fetch() par Axios dans le store Pinia
  • Afficher un squelette de chargement (v-skeleton-loader) pendant les requêtes
  • Afficher une alerte (v-alert) si l'API est indisponible
  • Créer un fichier mock JSON pour tester hors-ligne
  • Comprendre les 3 états d'un appel API : loading, error, data

Résultat attendu

Page d'accueil avec compteur, skeleton loaders et gestion d'erreurs

Contexte

Le store pokemonStore utilise encore fetch() pour charger les Pokémon. On va le migrer vers Axios pour profiter de la configuration centralisée (base URL, headers, gestion d'erreurs).

En parallèle, on va rendre l'interface robuste : au lieu d'un écran blanc quand l'API ne répond pas, l'utilisateur verra un message d'erreur clair.

Encart oral — Les 3 états d'un appel API

Tout appel réseau a exactement 3 états possibles :

  1. Loading — la requête est en cours (afficher un indicateur)
  2. Error — la requête a échoué (afficher un message d'erreur)
  3. Data — la requête a réussi (afficher les données)

Un bon développeur gère toujours ces 3 états. Ne jamais laisser l'utilisateur devant un écran vide sans explication.

Composants Vuetify utilisés

ComposantRôleDocumentation
<v-skeleton-loader>Affiche un placeholder animé pendant le chargementSkeleton Loaders
<v-alert>Affiche un message d'information, d'erreur ou d'avertissementAlerts

Tâches

1. Migrer le store vers Axios

Dans src/stores/pokemonStore.js, remplacez fetch() par Axios.

Avant (fetch) :

js
async fetchPokemons ({ withLoader = true } = {}) {
  if (withLoader) this.isLoading = true

  try {
    const response = await fetch('http://localhost:3535/pokemons') 
    this.pokemons = await response.json() 
  } catch (error) {
    console.error('Erreur:', error.message)
    this.pokemons = []
  } finally {
    if (withLoader) this.isLoading = false
  }
}

Après (Axios) :

js
import api from '@/plugins/axios'

// ... dans les actions :

async fetchPokemons ({ withLoader = true } = {}) {
  if (withLoader) this.isLoading = true

  try {
    const response = await api.get('/pokemons') 
    this.pokemons = response.data 
  } catch (error) {
    console.error('Erreur:', error.message)
    this.pokemons = []
  } finally {
    if (withLoader) this.isLoading = false
  }
}

Faites la même migration pour fetchTypes :

js
async fetchTypes ({ withLoader = true } = {}) {
  if (withLoader) this.isLoading = true

  try {
    const response = await api.get('/types') 
    this.types = response.data 
  } catch (error) {
    console.error('Erreur:', error.message)
    this.types = []
  } finally {
    if (withLoader) this.isLoading = false
  }
}

Notez les différences :

  • Import : import api from '@/plugins/axios' au lieu de rien (fetch est global)
  • Appel : api.get('/pokemons') au lieu de fetch('http://localhost:3535/pokemons')
  • Données : response.data au lieu de await response.json() (Axios parse automatiquement le JSON)
  • URL : /pokemons au lieu de l'URL complète (grâce à la baseURL)

2. Ajouter le skeleton loader dans index.vue

Le state isLoading du store est déjà mis à true pendant les requêtes. On va l'utiliser pour afficher des squelettes de chargement.

Dans src/pages/index.vue, ajoutez avant la grille de cartes :

vue
<!-- Squelettes de chargement pendant la requête API -->
<v-row v-if="pokemonStore.isLoading">
  <v-col
    v-for="n in 8"
    :key="n"
    cols="12"
    sm="6"
    md="4"
    lg="3"
  >
    <v-skeleton-loader
      type="image, article"
      height="350"
    />
  </v-col>
</v-row>

Le squelette simule la forme des cartes Pokémon. Le type="image, article" dessine un rectangle (image) suivi de lignes de texte (titre + sous-titre).

3. Ajouter l'alerte d'erreur

Toujours dans index.vue, ajoutez une alerte qui s'affiche quand la liste est vide et que le chargement est terminé :

vue
<!-- Message d'erreur si aucun Pokémon chargé -->
<v-alert
  v-else-if="pokemonStore.pokemons.length === 0"
  type="error"
  variant="tonal"
  class="mb-6"
>
  Impossible de charger les Pokémon. Vérifiez que l'API tourne sur
  {{ apiUrl }}.
</v-alert>

<!-- Grille de cartes (cas normal) -->
<v-row v-else>
  <!-- ... vos v-col + PokemonCard ... -->
</v-row>

Et dans le <script setup>, ajoutez la variable pour l'URL :

js
// URL de l'API pour le message d'erreur
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3535'

Les trois blocs (v-if, v-else-if, v-else) couvrent les 3 états :

v-if="pokemonStore.isLoading"          → Loading (skeleton)
v-else-if="pokemons.length === 0"      → Error (alert)
v-else                                  → Data (grille)

4. Créer le fichier mock JSON

Créez public/mock.json avec quelques Pokémon de test :

json
{
  "types": [
    { "id": 1, "name": "Normal", "color": "#A8A878" },
    { "id": 2, "name": "Feu", "color": "#F08030" },
    { "id": 3, "name": "Eau", "color": "#6890F0" },
    { "id": 4, "name": "Plante", "color": "#78C850" },
    { "id": 5, "name": "Électrique", "color": "#F8D030" }
  ],
  "pokemons": [
    {
      "id": "1",
      "name": "Pikachu",
      "types": [5],
      "level": 35,
      "img": "pikachu.png",
      "description": "Quand plusieurs de ces Pokémon se réunissent, leur énergie peut provoquer de violents orages.",
      "stats": { "hp": 35, "attack": 55, "defense": 40, "speed": 90 }
    },
    {
      "id": "2",
      "name": "Bulbizarre",
      "types": [4],
      "level": 12,
      "img": "bulbizarre.png",
      "description": "Il a une étrange graine plantée sur son dos.",
      "stats": { "hp": 45, "attack": 49, "defense": 49, "speed": 45 }
    },
    {
      "id": "3",
      "name": "Carapuce",
      "types": [3],
      "level": 8,
      "img": "carapuce.png",
      "description": "Son dos rond est recouvert d'une carapace qui lui sert de protection.",
      "stats": { "hp": 44, "attack": 48, "defense": 65, "speed": 43 }
    },
    {
      "id": "4",
      "name": "Dracaufeu",
      "types": [2],
      "level": 60,
      "img": "dracaufeu.png",
      "description": "Il crache du feu si fort qu'il fait fondre les rochers.",
      "stats": { "hp": 78, "attack": 84, "defense": 78, "speed": 100 }
    },
    {
      "id": "5",
      "name": "Rondoudou",
      "types": [1],
      "level": 20,
      "img": "rondoudou.png",
      "description": "Quand ses grands yeux s'illuminent, il chante une berceuse mystérieuse.",
      "stats": { "hp": 115, "attack": 45, "defense": 20, "speed": 20 }
    }
  ]
}

Ce fichier est servi directement par Vite sur /mock.json. Il permet de tester l'application même si l'API est coupée, en changeant temporairement l'URL dans le store :

js
// Temporairement, pour tester sans API :
const response = await api.get('/mock.json')
this.pokemons = response.data.pokemons

5. Tester les 3 états

  1. État normal : lancez l'API et l'app → les Pokémon s'affichent
  2. État loading : ouvrez DevTools → onglet Network → activez le throttle "Slow 3G" → rechargez la page → les squelettes s'affichent
  3. État error : arrêtez l'API (Ctrl+C dans le terminal de l'API) → rechargez la page → l'alerte d'erreur s'affiche

Tests

  • Le store utilise api.get() au lieu de fetch()
  • Les squelettes s'affichent pendant le chargement
  • L'alerte d'erreur s'affiche quand l'API est coupée
  • L'application fonctionne normalement quand l'API tourne
  • Le fichier mock.json est accessible sur /mock.json
  • 0 erreurs dans la console (quand l'API tourne)

Solutions

src/stores/pokemonStore.js (actions modifiées)
js
import { defineStore } from 'pinia'
import api from '@/plugins/axios'

export const usePokemonStore = defineStore('pokemon', {
  state: () => ({
    isLoading: false,
    types: [],
    pokemons: [],
  }),

  getters: {
    totalPokemons: state => state.pokemons.length,

    getTypeById: state => typeId => {
      return state.types.find(type => type.id === typeId)
    },

    getPokemonById: state => pokemonId => {
      return state.pokemons.find(pokemon => pokemon.id === pokemonId)
    },
  },

  actions: {
    async init () {
      console.log('Initialisation du store Pokémon...')
      this.isLoading = true

      try {
        await Promise.all([
          this.fetchTypes({ withLoader: false }),
          this.fetchPokemons({ withLoader: false }),
        ])
        console.log('Store Pokémon 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 = []
        }
      } catch (error) {
        console.error('Erreur lors du chargement des Pokémon:', error.message)
        this.pokemons = []
      } finally {
        if (withLoader) this.isLoading = false
      }
    },
  },
})
src/pages/index.vue (version avec les 3 états)
vue
<template>
  <v-container>
    <h1 class="text-h3 text-center my-6">
      Pokédex
      <span class="text-subtitle-1">({{ pokemonStore.totalPokemons }})</span>
    </h1>

    <!-- État 1 : Loading — squelettes de chargement -->
    <v-row v-if="pokemonStore.isLoading">
      <v-col
        v-for="n in 8"
        :key="n"
        cols="12"
        sm="6"
        md="4"
        lg="3"
      >
        <v-skeleton-loader
          type="image, article"
          height="350"
        />
      </v-col>
    </v-row>

    <!-- État 2 : Error — alerte si aucun Pokémon chargé -->
    <v-alert
      v-else-if="pokemonStore.pokemons.length === 0"
      type="error"
      variant="tonal"
      class="mb-6"
    >
      Impossible de charger les Pokémon. Vérifiez que l'API tourne sur
      {{ apiUrl }}.
    </v-alert>

    <!-- État 3 : Data — grille de cartes Pokémon -->
    <v-row v-else>
      <v-col
        v-for="pokemon in pokemonStore.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 { usePokemonStore } from '@/stores/pokemonStore'
import PokemonCard from '@/components/PokemonCard.vue'

const pokemonStore = usePokemonStore()

// URL de l'API pour le message d'erreur
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3535'
</script>

Commit

bash
git add -A
git commit -m "feat: migration Axios, skeleton loader et gestion erreurs API"

Documentation pour les cours de développement web