• 15 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 01/12/2023

Interagissez avec des API

Nous avons déjà vu comment manipuler des actions asynchrones dans le dernier chapitre de la partie 2. N’hésitez pas à relire ce chapitre car nous allons réutiliser ce que nous avons vu pour récupérer et stocker la liste des freelances depuis l’API.

Définissez le state

Vous vous souvenez du hook useFetch  présent dans le code de Shiny ? On y trouve les states suivants :

const [data, setData] = useState({})
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(false)

On note que plusieurs states sont utilisés : pour savoir si la requête est en cours  (  isLoading  ), si une erreur est survenue  (  error  ) et enfin pour stocker le résultat  (  data  ).

Dans notre state Redux, nous allons avoir besoin de stocker les mêmes informations. Nous allons cependant utiliser une propriété status  qui aura une de ces 5 valeurs :

  • “void” si la requête n’a pas encore été lancée ;

  • “pending” si la requête est en cours ;

  • “resolved” si la requête a retourné un résultat ;

  • “rejected” si la requête a échoué ;

  • “updating” la requête a retourné un résultat mais qu'une nouvelle requête est en cours pour mettre à jour les données.

Pourquoi on a besoin de ce cinquième status “updating” ?

Les données que l’on stocke dans Redux y restent jusqu’au rechargement de la page. Il faut donc vérifier de temps en temps que le contenu est toujours à jour en lançant de nouveau la requête !

Le state initial va donc ressembler à cela :

const initialState = {
    status: 'void',
    data: null,
    error: null,
}

Définissez les actions

Nous allons avoir besoin de 3 actions :

  1. freelances/fetching” : la requête a été lancée ;

  2. freelances/resolved” : la requête a retourné un résultat (envoyé dans le payload  de l’action) ;

  3. freelances/rejected” : la requête a échoué (erreur dans le payload  )

const FETCHING = 'freelances/fetching'
const RESOLVED = 'freelances/resolved'
const REJECTED = 'freelances/rejected'
 
const freelancesFetching = () => ({ type: FETCHING });
const freelancesResolved = (data) => ({ type: RESOLVED, payload: data });
const freelancesRejected = (error) => ({ type: REJECTED, payload: error });

Créez le Reducer

Pour créer ce reducer, nous allons utiliser un switchplutôt que des if  . Le résultat est le même, mais le code est un peu plus facile à lire ! Nous allons également englober tous les reducers dans produce  d’Immer (sans oublier de l’installer avant !).

Voyons à présent comment créer le reducer :

Vous pouvez retrouver le code de cette vidéo sur la branche P3C3S3-freelances-actions-reducer du repository à cette adresse.

Créez l’action asynchrone

Notre action asynchrone va utiliser les mêmes principes que l’action autoplay  de Tennis Score. On utilise getState  et dispatch  pour interagir avec Redux. Ici, on utilise une fonction async  pour simplifier la manipulation des promesses :

export async function fetchOrUpdateFreelances(store) {
    const status = selectFreelances(store.getState()).status
    if (status === 'pending' || status === 'updating') {
        return
    }
    store.dispatch(freelancesFetching())
    try {
        const response = await fetch('http://localhost:8000/freelances')
        const data = await response.json()
        store.dispatch(freelancesResolved(data))
    } catch (error) {
        store.dispatch(freelancesRejected(error))
    }
}

Il ne reste plus qu'à utiliser cette action dans l’application à la place de useFetch  . On va donc lancer l’action fetchOrUpdateFreelances  dans un useEffect  :

function Freelances() {
    const store = useStore();
    useEffect(() => {
        fetchOrUpdateFreelances(store);
    }, [store]);
    // ...
}

Pourquoi utilise-t-on un useEffect  ? On ne peut pas exécuter la fonction fetchOrUpdateFreelances  directement dans le composant ?

Non ! Lancer une action Redux est considéré comme un effet de bord (side-effect) dans React, il faut donc toujours le faire soit dans un événement (  onClick  , par exemple), soit dans un useEffect  .

Pourquoi on envoie le store  en dépendance du useEffect  ?

Le plugin ESLint impose que tout ce qui est utilisé dans l’effet soit passé en dépendance ; comme on utilise le store  , on le passe en dépendance. Pas de panique cependant, car le store est toujours le même ! Le useEffect  ne sera donc jamais réexécuté. La fonction fetchOrUpdateFreelances  n'est pas concernée car elle est définie en dehors du composant.

 Voyons ensuite comment utiliser cette action dans un  useEffect  :

Vous pouvez retrouver le code de cette vidéo sur la branche P3C3S4-freelances-use-effect du repository à cette adresse.

Normalisez les données

Dans une application Redux conséquente, le state devient complexe et il est donc important de bien l’organiser.

Il est notamment recommandé de normaliser les données. Normaliser les données, cela signifie organiser ces dernières un peu comme dans une base de données : on sépare les différents types de données, on les identifie grâce à un id  et on fait des références entre les éléments.

Voici un exemple de state normalisé pour une application de recettes organisées en catégories :

const initialState = {
 categories: {
   DB0y54: { id: "DB0y54", name: "Apéritif" },
   vgVATs: { id: "vgVATs", name: "Desserts" },
   udFvZM: { id: "udFvZM", name: "Entrees" },
   iDYQt1: { id: "iDYQt1", name: "Plats" },
   oWsxwi: { id: "oWsxwi", name: "Boissons" },
 },
 recipes: {
   JK0lWf: { id: "JK0lWf", name: "Citronnade", categories: ["oWsxwi"] },
   SkbKhJ: { id: "SkbKhJ", name: "Smoothie", categories: ["oWsxwi"] },
   wAY3de: { id: "wAY3de", name: "Milkshake", categories: ["oWsxwi", "vgVATs"] },
   q8IYAN: { id: "q8IYAN", name: "Paris Brest", categories: ["vgVATs"] },
   MpMYU0: { id: "MpMYU0", name: "Pain perdu", categories: ["vgVATs"] },
   wzmJkc: { id: "wzmJkc", name: "Pizza rolls", categories: ["DB0y54"] },
   iQujrs: { id: "iQujrs", name: "Focaccia", categories: ["DB0y54"] },
   BEdrqG: { id: "BEdrqG", name: "Soufflé au fromage", categories: ["udFvZM", "iDYQt1"] },
   r1GSrx: { id: "r1GSrx", name: "Taboulé", categories: ["udFvZM"] },
   XzBVGt: { id: "XzBVGt", name: "Pâte carbonara", categories: ["iDYQt1"] },
   MKP5ig: { id: "MKP5ig", name: "Risotto", categories: ["iDYQt1"] },
 },
};

L'intérêt de ce type d’organisation est qu'il est très facile d'accéder à un élément à partir de son id  :

const selectCategory = (categoryId) => (state) => state.categories[categoryId];
const selectRecipe = (recipeId) => (state) => state.recipes[recipeId];

Pour accéder à des éléments liés, comme les recettes d’une catégorie, il faudra ajouter un peu de logique dans le selector :

const selectCategoryRecipes = (categoryId) => (state) => {
    return state.recipes.filter((recipe) =>
        recipe.categories.include(categoryId)
    );
};

Cette technique est parfaite pour les states qui existent uniquement dans le navigateur. Cependant, attention, car elle ne fonctionne pas très bien avec les states provenant d’un serveur : ce dernier renvoie presque toujours une représentation incomplète des données.

Par exemple, une URLhttps://api.recipes.com/recipespeut retourner une liste de recettes avec name  et id  , mais l’URLhttps://api.recipes.com/recipe/[recipe-id]retourne un objet avec plus d’informations, comme ingredients  et steps  .

C’est le cas de la PokéAPI, une API pokemon open source. Cette URL  renvoie bien moins d’informations sur chaque pokémon que cette autre URL

Pour les données en provenance d’une API, il donc recommandé de :

  • Stocker le résultat de chaque URL séparément.

  • Stocker dans le state l’intégralité du résultat de la requête.

  • Ne pas modifier le contenu du state manuellement (mais on peut le mettre à jour en relançant la requête).

  • Utiliser un status  pour suivre l’état de la requête, comme on l’a vu avec la liste de freelances.

Voici à quoi pourrait ressembler le state d’une application qui utilise la PokeApi :

const initialState = {
    // https://pokeapi.co/api/v2/pokemon
    pokemons: { status: 'void' },
    pokemon: {
        // https://pokeapi.co/api/v2/pokemon/1/
        1: { status: 'pending' },
        // https://pokeapi.co/api/v2/pokemon/4/
        4: { status: 'void' },
    }
}

Exercez-vous

À vous de mettre en place de la logique asynchrone dans Shiny. Dans cet exercice, vous allez devoir :

  • Utiliser Redux à la place de useFetch  pour récupérer la liste des freelances sur la page /freelances  , et les questions sur la page Survey  .

  • Utiliser Redux à la place de fetch  pour récupérer les détails d’un freelance sur la page /profile/:id  .

Une fois que vous avez terminé, allez jeter un œil à la version corrigée sur la branche P3C3S6-solution du repository React-Redux-Shiny ici.

Vous pouvez également regarder la vidéo ci-dessous, qui explique le code de la correction.

En résumé

  • Pour interagir avec une API dans Redux, on utilise un state qui contient une propriété status  pour suivre l’état de la requête.

  • On utilise des actions pour suivre l’évolution de la requête et mettre à jour le state.

    • Lorsque que la requête est envoyée, on dispatch l’action pending  .

    • Si la requête réussit, on dispatch l’action resolved  avec les données en payload.

    • Si la requête échoue, on dispatch l’action rejected  avec l’erreur en payload.

  • Pour accéder au store Redux depuis un composant, on utilise le hook useStore  .

Si vous avez suivi le cours jusque là, bravo ! Vous connaissez désormais Redux, et vous devriez être capable de mettre en place un système de state management avancé dans n’importe quel projet !

Mais vous l’avez probablement remarqué, le cours n’est pas terminé ! Dans la prochaine et dernière partie, je vous propose d’aller un peu au-delà de Redux pour découvrir des outils qui facilitent le state management. Ce sera aussi l’occasion de découvrir comment on peut tester notre code Redux.

Exemple de certificat de réussite
Exemple de certificat de réussite