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 :
“freelances/fetching” : la requête a été lancée ;
“freelances/resolved” : la requête a retourné un résultat (envoyé dans le
payload
de l’action) ;“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 switch
plutô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/recipes
peut 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 pageSurvey
.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.