
Redux : Concepts clés (Store, Actions, Reducers, Dispatch, Middleware - Thunk/Saga)
Comprenez les concepts clés de Redux : le Store unique, les Actions descriptives, les Reducers purs, la fonction Dispatch et le rôle des Middlewares (Thunk, Saga).
Les piliers de l'architecture redux : Un flux de données prévisible
Redux est une bibliothèque de gestion d'état populaire, inspirée par l'architecture Flux de Facebook. Elle vise à rendre les changements d'état prévisibles en imposant certaines contraintes sur la manière dont l'état peut être mis à jour. Redux repose sur trois principes fondamentaux :
1. Source unique de vérité : L'état global de votre application est stocké dans un seul objet JavaScript, appelé le Store.
2. L'état est en lecture seule : La seule façon de modifier l'état est d'émettre (ou de "dispatcher") une Action, un objet décrivant ce qui s'est passé.
3. Les modifications sont faites avec des fonctions pures : Pour spécifier comment l'arbre d'état est transformé par les actions, vous écrivez des fonctions pures appelées Reducers.
Ces principes créent un flux de données unidirectionnel strict qui facilite la compréhension, le débogage et le test des applications. Examinons les concepts clés plus en détail.
Le store : Le coeur de l'application
Le Store est l'objet central qui détient l'état (state) complet de votre application. Il n'y a généralement qu'un seul store dans une application Redux. Le store est plus qu'un simple conteneur de données ; il a plusieurs responsabilités :
- Contient l'état de l'application.
- Permet l'accès à l'état via une méthode `getState()`.
- Permet à l'état d'être mis à jour via une méthode `dispatch(action)`.
- Enregistre les écouteurs (listeners) via `subscribe(listener)` pour être notifié des changements d'état.
- Désenregistre les écouteurs via la fonction retournée par `subscribe(listener)`.
Vous ne créez pas le store directement très souvent, surtout avec des outils comme Redux Toolkit qui simplifient ce processus, mais il est essentiel de comprendre son rôle central.
Les actions : Décrire les événements
Les Actions sont des objets JavaScript simples qui représentent une intention de modifier l'état. Elles sont la seule source d'information pour le store. Elles doivent obligatoirement avoir une propriété `type`, généralement une chaîne de caractères descriptive (souvent en majuscules), qui indique le type d'action effectuée. Conventionnellement, les actions peuvent aussi avoir une propriété `payload` qui contient les données nécessaires pour effectuer la modification de l'état.
// Exemple d'action pour ajouter un élément à une liste
{
type: 'todos/todoAdded', // Format recommandé par Redux Toolkit: 'domaine/typeAction'
payload: { id: 1, text: 'Apprendre Redux', completed: false }
}
// Exemple d'action pour incrémenter un compteur
{
type: 'counter/incremented'
}
// Exemple d'action pour définir un utilisateur
{
type: 'user/setUser',
payload: { id: 'abc', name: 'Alice' }
}Pour éviter les erreurs de frappe et faciliter la maintenance, on utilise souvent des "créateurs d'actions" (action creators), qui sont simplement des fonctions retournant un objet action :
// Créateur d'action
function addTodo(text) {
return {
type: 'todos/todoAdded',
payload: { id: Date.now(), text, completed: false } // Génère un ID simple ici
};
}
// Utilisation:
const action = addTodo('Finir ce chapitre');
// action contient { type: 'todos/todoAdded', payload: { id: ..., text: 'Finir ce chapitre', completed: false } }(Redux Toolkit simplifie grandement la création d'actions et de créateurs d'actions).
Les reducers : Calculer le nouvel état
Les Reducers sont le coeur de la logique de mise à jour dans Redux. Ce sont des fonctions pures qui prennent deux arguments : l'état actuel (`state`) et l'action (`action`) qui vient d'être dispatchée. Ils analysent l'action et décident comment l'état doit changer en réponse. Un reducer doit toujours :
1. Retourner une nouvelle version de l'état si une modification est nécessaire. Il ne doit jamais muter (modifier directement) l'état précédent.
2. Retourner l'état précédent inchangé si l'action ne le concerne pas ou si aucune modification n'est requise.
3. Ne pas effectuer d'opérations avec effets de bord (side effects) comme des appels API, des `console.log` aléatoires, ou l'utilisation de `Math.random()` ou `Date.now()` à l'intérieur. Il doit être entièrement prévisible : pour un même état et une même action en entrée, il doit toujours produire la même sortie.
// Exemple simple de reducer pour un compteur
const initialState = { value: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/incremented':
// Retourne un NOUVEL objet état
return { ...state, value: state.value + 1 };
case 'counter/decremented':
return { ...state, value: state.value - 1 };
case 'counter/incrementByAmount':
// Utilise le payload de l'action
return { ...state, value: state.value + action.payload };
default:
// Si l'action n'est pas reconnue, retourne l'état actuel
return state;
}
}
Dans une application plus grande, on combine souvent plusieurs reducers, chacun gérant une partie spécifique ("slice") de l'état global, à l'aide de la fonction `combineReducers` (ou des outils de Redux Toolkit).
Dispatch : Envoyer les actions au store
Maintenant que nous avons des actions et des reducers, comment déclencher une mise à jour d'état ? C'est le rôle de la fonction `dispatch`, qui est une méthode fournie par le store Redux (`store.dispatch(action)`).
Lorsque vous appelez `dispatch` avec un objet action, le store Redux fait deux choses :
1. Il passe l'état actuel et l'action fournie à la fonction reducer racine.
2. Il sauvegarde la nouvelle valeur d'état retournée par le reducer.
Dans une application React, vous n'appelez généralement pas `store.dispatch()` directement. La bibliothèque `react-redux` fournit un hook `useDispatch` qui vous donne accès à la fonction `dispatch` du store depuis vos composants.
import React from 'react';
import { useDispatch } from 'react-redux';
import { incremented, decremented } from './counterSlice'; // Actions créées avec Redux Toolkit
function Compteur() {
const dispatch = useDispatch();
return (
{/* Affiche la valeur du compteur (via useSelector, vu plus tard) */}
);
}Ici, cliquer sur les boutons déclenche l'appel à `dispatch` avec les actions correspondantes, initiant ainsi le processus de mise à jour de l'état.
Middleware : Intercepter les actions pour les effets de bord
Le flux décrit jusqu'à présent (dispatch -> reducer -> nouvel état) est synchrone et pur. Mais que faire des opérations asynchrones comme les appels API ? Les reducers doivent rester purs et ne peuvent pas contenir d'effets de bord.
C'est là qu intervient le concept de Middleware. Un middleware Redux est une fonction qui s'intercale entre le moment où une action est dispatchée et le moment où elle atteint le reducer. Il peut intercepter l'action, effectuer des opérations (y compris asynchrones), dispatcher d'autres actions, ou laisser l'action originale continuer son chemin vers le reducer.
Les middlewares sont le point d'extension privilégié de Redux pour gérer la logique asynchrone, le logging, le routage et d'autres effets de bord.
Deux middlewares très populaires pour la gestion asynchrone sont :
- Redux Thunk : Le plus simple. Il permet de dispatcher des *fonctions* (appelées "thunks") au lieu de simples objets action. Ces fonctions reçoivent `dispatch` et `getState` en arguments et peuvent effectuer de la logique asynchrone (comme un `fetch`), puis dispatcher des actions synchrones une fois l'opération terminée (par exemple, une action `pending`, puis `fulfilled` ou `rejected`).
- Redux Saga : Une solution plus puissante et complexe, basée sur les Generateurs ES6. Elle permet de gérer des flux asynchrones complexes (annulations, délais, écoute d'actions spécifiques) de manière plus déclarative et testable, en isolant complètement les effets de bord de la logique applicative.
Redux Toolkit est configuré par défaut avec Redux Thunk et fournit des outils comme `createAsyncThunk` pour simplifier l'écriture de la logique asynchrone.
Comprendre ces concepts clés – Store, Actions, Reducers, Dispatch et le rôle des Middlewares – est fondamental pour travailler efficacement avec Redux et construire des applications dont l'état est gérable et prévisible.