Découvrez les middlewares Redux
Les middlewares Redux permettent de changer le fonctionnement du store. Cela permet par exemple d’ajouter un système de logging, ou bien de faciliter la manipulation d’action asynchrone.
Un middleware Redux est une fonction qui ressemble à ça :
const myMiddleware = store => next => action => {
// Ici on peut manipuler l'action pour changer
// le comportement de Redux !
return next(action)
}
Qu’est-ce que c’est que toutes ces flèches ( =>
) ?
Les flèches sont en fait des fonctions. Seulement, ici on a une fonction qui retourne une fonction qui retourne une fonction ! On aurait également pu écrire le code de cette manière :
function myMiddleware(store) {
return function (next) {
return function (action) {
// Ici on peut manipuler l'action pour changer
// le comportement de Redux !
return next(action);
};
};
}
Cette syntaxe un peu étrange est liée au fonctionnement des middlewares. Si vous voulez comprendre pourquoi on utilise toutes ces fonctions (et que vous comprenez l’anglais), je vous invite à lire la page dédiée au middleware dans la documentation Redux ici.
Ce qui va nous intéresser ici, ce sont les variables next
et action
:
action
correspond à une action envoyée au store pardispatch
.next
est une fonction qui va envoyer l’action au prochain middleware ; si le middleware est le dernier,next
va envoyer l’action au reducer pour mettre à jour le state.
On va donc pouvoir utiliser ces deux variables pour intercepter l’action et modifier le comportement de Redux.
Voici un premier exemple plutôt simple d’un middleware qui affiche chaque action dans la console :
const logMiddleware = store => next => action => {
// on affiche chaque action dans la console
console.log(action);
return next(action)
}
Et voilà un autre middleware qui ignore certaines actions et affiche un warning dans la console :
const deprecatedActionsMiddleware = store => next => action => {
if (action.type = "menu/hide") {
console.warn(`Laction "menu/hide" est dépréciée, veuillez utiliser l'action "menu/compactMode" à la place`);
// on n'exécute pas next pour ne pas envoyer l'action au reducer !
return;
}
// sinon on envoie l'action au reducer
return next(action)
}
Maintenant que nos middlewares sont créés, il va falloir les intégrer à la configuration du store
. Pour cela, on peut utiliser la fonction configureStore
de Redux-Toolkit qui accepte un paramètre middleware
:
configureStore({
reducer: { /* ... */ },
middleware: [
logMiddleware,
deprecatedActionsMiddleware
]
})
D'ailleurs, Redux-Toolkit utilise trois middlewares bien pratiques par défaut :
Redux-immutable-state-invariant (actif uniquement en développement) pour vérifier que l’on ne fait pas de mutations accidentelles dans le reducer.
Serializability Middleware (actif uniquement en développement) pour vérifier que le state et les actions ne contiennent pas des données non sérialisables qui peuvent poser des problèmes avec les Devtools, par exemple.
Redux-Thunk qui permet de faciliter la manipulation d’action asynchrone. C’est le middleware Redux le plus utilisé, c’est pourquoi la section suivante lui est dédiée.
Attention, cependant, car lorsque l’on passe un tableau à l’option middleware
de configureStore
, ce dernier remplace les middlewares par défaut. On peut facilement résoudre ce problème en utilisant la fonction getDefaultMiddleware
de Redux-Toolkit :
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
export default configureStore({
reducer: { /* ... */ },
middleware: [
...getDefaultMiddleware(),
logMiddleware,
deprecatedActionsMiddleware
]
})
Utilisez le thunk middleware
Redux-Thunk est le middleware le plus populaire pour Redux. Il permet de simplifier la manipulation des actions asynchrones.
Pour rappel, jusqu’à maintenant, nous avons passé le store en paramètre des actions asynchrones pour accéder à getState
et dispatch
. Voici, par exemple, l’action fetchOrUpdateFreelances
:
async function fetchOrUpdateFreelances(store) {
// ...
}
Pour accéder au store, on a ensuite utilisé le hook useStore
dans le composant.
Avec le thunk middleware, au lieu de passer le store directement, on va pouvoir donner notre fonction directement à Redux (par défaut, Redux accepte uniquement des objets actions). Le thunk middleware va alors appeler cette fonction avec dispatch
et getState
en paramètre.
On peut comprendre le fonctionnement de Redux-Thunk en lisant le code source qui ne fait que quelques lignes :
const thunkMiddleware = (store) => (next) => (action) => {
// si l'action est une fonction...
if (typeof action === 'function') {
// on l'exécute avec `dispatch` et `getState` en paramètre
return action(store.dispatch, store.getState)
}
// sinon on envoie l'action au reducer
return next(action)
}
Une fois Redux-Thunk installé (c’est le cas par défaut avec Redux-Toolkit), on peut donc envoyer la fonction fetchOrUpdateFreelances
directement avec dispatch
. Attention toutefois car il faut changer un peu la fonction pour qu’elle accepte dispatch
et getState
au lieu du store
en paramètre.
export async function fetchOrUpdateFreelances(dispatch, getState) {
const status = selectFreelances(getState()).status
if (status === 'pending' || status === 'updating') {
return
}
dispatch(freelancesFetching())
try {
const response = await fetch('http://localhost:8000/freelances')
const data = await response.json()
dispatch(freelancesResolved(data))
} catch (error) {
dispatch(freelancesRejected(error))
}
}
Les fonctions acceptées par le thunk middleware sont appelées des thunks, d’où le nom du middleware. On peut maintenant utiliser le thunk défini ci-dessus comme suit dans nos composants :
function Freelances() {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchOrUpdateFreelances)
}, [dispatch])
// ...
}
Transformez vos actions en thunks à l’aide des thunk creators
Maintenant que c’est Redux-Thunk qui exécute les actions asynchrones, comment faire pour passer des paramètres ?
Lorsqu’un thunk a besoin d’un paramètre autre que dispatch
et getState
, il faut passer par un thunk creator !
Prenons, par exemple, la fonction fetchOrUpdateFreelance
:
export async function fetchOrUpdateFreelance(store, freelanceId) {
// ...
}
Si on transforme cette fonction en thunk, on perd le paramètre freelanceId
; il va donc falloir créer une fonction qui retourne un thunk :
export function fetchOrUpdateFreelance(freelanceId) {
// on retourne un thunk
return async (dispatch, getState) => {
// ...
const selectFreelanceById = selectFreelance(freelanceId)
const status = selectFreelanceById(getState()).status
if (status === 'pending' || status === 'updating') {
return
}
dispatch(freelanceFetching(freelanceId))
try {
const response = await fetch(
`http://localhost:8000/freelance?id=${freelanceId}`
)
const data = await response.json()
dispatch(freelanceResolved(freelanceId, data))
} catch (error) {
dispatch(freelanceRejected(freelanceId, error))
}
}
}
Tout comme pour les action creators, il faudra exécuter le thunk creator pour l’envoyer à dispatch
:
function Profile() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchOrUpdateFreelance(freelanceId))
}, [dispatch, freelanceId])
//...
}
Je vous propose de mettre tout cela en pratique dans la vidéo qui suit :
Vous pouvez retrouver le code de cette vidéo sur la branche P4C2S3-thunks du repository à cette adresse.
Exercez-vous
Prêt à mettre en pratique ? Dans cet exercice, vous allez devoir :
Utiliser les thunks pour les actions asynchrones existantes (
fetchOrUpdateFreelance
,fetchOrUpdateFreelances
,fetchOrUpdateSurvey
).Déplacer dans Redux la logique qui permet de récupérer les résultats du quiz (en utilisant Redux-Toolkit et les thunks, bien évidemment !)
Une fois que vous avez terminé, allez jeter un œil à la version corrigée sur la branche P4C2S4-solution du repository React-Redux-Shiny à cette adresse.
Vous trouverez également ci-dessous une vidéo d’explication du code de la correction.
En résumé
Les middlewares permettent d’étendre les fonctionnalités de Redux.
Redux-Thunk est le middleware le plus populaire. Il permet de faciliter la gestion des actions asynchrones.
Un thunk est une fonction qui reçoit
dispatch
etgetState
en paramètre. Avec Redux-Thunk, on peut envoyer les thunks directement à la fonctiondispatch
du store !
Nous avons vu dans un chapitre précédent les fonctions createAction
et createReducer
de Redux-Toolkit, qui permettent de simplifier l’écriture du code Redux. Dans le prochain chapitre, nous allons découvrir la fonctionnalité de slice de Redux-Toolkit, qui permet de réduire encore plus le boilerplate de la logique Redux !