Article original : Data Flow in Redux Explained – A State Management Handbook
Dans les applications React complexes, la gestion efficace de l'état de l'application peut devenir un défi. C'est là qu'intervient Redux, une bibliothèque de gestion d'état prévisible.
En introduisant un flux de données unidirectionnel, Redux apporte de l'ordre et de la clarté dans la manière dont les données sont mises à jour et interagissent au sein de vos composants React.
Cet article traite du fonctionnement interne de Redux, en se concentrant spécifiquement sur la manière dont les données circulent dans votre application. Nous explorerons les concepts clés tels que le Redux store, les actions, les reducers et les selectors, ainsi que des exemples pratiques de la manière dont ils collaborent pour gérer de manière fluide l'état de votre application.
Table des matières
- Qu'est-ce que Redux ?
- Pourquoi utiliser Redux pour la gestion des données ?
- Concepts fondamentaux du flux de données Redux
- Flux de données unidirectionnel
- Avantages du flux de données unidirectionnel
- Gestion d'état avec le Redux Store
- Qu'est-ce que le Redux Store ?
- Structure du Store (State, Reducers, Actions)
- Actions : Initier des changements d'état
- Action Creators (Fonctions pour créer des actions)
- Action Types (Identifier les différentes actions)
- Comment traiter les changements d'état
- Fonctions pures : les Reducers au cœur du système
- Caractéristiques des fonctions pures
- Anatomie d'une fonction Reducer
- Paramètres : État précédent et objet Action
- Valeur de retour : État mis à jour
- Comment gérer différentes actions dans les Reducers
- Utilisation des instructions Switch ou de la logique conditionnelle
- Dispatcher des actions : Comment mettre à jour le Redux Store
- La fonction
dispatch - Dispatcher des actions depuis des composants ou des événements
- Comment accéder à des données spécifiques du Store
- Créer des fonctions Selector
- Mémoïsation pour une utilisation efficace des Selectors
- Comment connecter des composants React à Redux
- La fonction
connectde la bibliothèquereact-redux - Mappage du State et du Dispatch aux Props
- Utiliser des composants connectés dans votre application
- Techniques avancées de flux de données Redux
- Actions asynchrones (Redux Thunk, Redux Saga)
- Middleware pour étendre les fonctionnalités de Redux
- Bonnes pratiques pour gérer le flux de données dans Redux
- Conclusion
Qu'est-ce que Redux ?
Redux est un conteneur d'état prévisible pour les applications JavaScript, principalement utilisé avec des bibliothèques comme React. Il aide à gérer l'état de l'application dans un store centralisé, ce qui facilite la gestion et la mise à jour de l'état dans toute votre application.
En termes simples, Redux fournit un moyen de stocker et de gérer les données dont votre application a besoin pour fonctionner. Il suit un modèle strict pour garantir que les changements d'état sont prévisibles et gérables.
Pourquoi utiliser Redux pour la gestion des données ?
L'utilisation de Redux pour la gestion des données dans votre application offre plusieurs avantages :
Gestion centralisée de l'état : Redux stocke l'état de l'application dans un store unique, ce qui facilite la gestion et le débogage par rapport à un état dispersé dans plusieurs composants.
Changements d'état prévisibles : Les mutations d'état sont effectuées via des reducers, qui sont des fonctions pures. Cela garantit que les changements d'état sont prévisibles et traçables, ce qui permet de mieux comprendre comment les données circulent dans votre application.
Débogage facilité : Avec une source unique de vérité, le débogage devient plus simple. Vous pouvez journaliser les changements d'état, suivre les actions et même implémenter un débogage par "voyage dans le temps" (via Redux DevTools) pour rejouer les actions et inspecter l'état à n'importe quel moment.
Facilite les tests : Comme les reducers sont des fonctions pures qui dépendent uniquement de leur entrée et produisent une sortie prévisible, les tests deviennent simples. Vous pouvez facilement tester comment les reducers mettent à jour l'état en réponse à différentes actions.
Impose un flux de données unidirectionnel : Redux suit un modèle de flux de données unidirectionnel strict. Les données circulent dans une seule direction : les actions sont dispatchées, les reducers mettent à jour l'état de manière immuable, et les composants s'abonnent aux changements qui les intéressent. Ce modèle simplifie la gestion des données et réduit les bugs liés à un état incohérent.
Facilite la persistance de l'état : Redux permet de persister plus facilement l'état de votre application d'une session à l'autre ou de le stocker localement, améliorant ainsi l'expérience utilisateur en préservant les données entre les visites.
Scalabilité : Redux s'adapte bien aux grandes applications grâce à sa gestion centralisée de l'état. À mesure que votre application grandit, la gestion de l'état devient plus gérable et moins sujette aux erreurs par rapport à l'utilisation de l'état local des composants ou au "prop drilling".
Concepts fondamentaux du flux de données Redux
Comprendre les concepts de base du flux de données Redux est essentiel pour maîtriser la gestion d'état dans les applications JavaScript modernes.
Flux de données unidirectionnel
Redux suit un modèle de flux de données unidirectionnel strict, ce qui signifie que les données de votre application se déplacent dans une seule direction à travers une série d'étapes :
- Actions : 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.
- Reducers : Les reducers sont des fonctions pures chargées de gérer les transitions d'état basées sur les actions. Ils spécifient comment l'état de l'application change en réponse aux actions envoyées au store.
- Store : Le store contient l'état de l'application. Il permet d'accéder à l'état via
getState(), de mettre à jour l'état viadispatch(action)et d'enregistrer des écouteurs viasubscribe(listener). - View : Les composants React (ou toute autre couche UI) s'abonnent au store pour recevoir des mises à jour lorsque l'état change. Ils se re-rendent ensuite en fonction de l'état mis à jour.
Voici un aperçu simplifié du fonctionnement du flux de données unidirectionnel dans Redux :
- Dispatch de l'action : Les composants dispatchent des actions au Redux store en utilisant
store.dispatch(action). Les actions sont des objets JavaScript simples avec un champtypequi décrit le type d'action effectuée. - Traitement de l'action : Le store transmet l'action dispatchée au reducer racine. Le reducer est une fonction pure qui prend l'état actuel et l'action, calcule le nouvel état en fonction de l'action et renvoie l'état mis à jour.
- Mise à jour de l'état : Le Redux store met à jour son état en fonction de la valeur de retour du reducer racine. Il informe tous les composants abonnés du changement d'état.
- Re-rendu du composant : Les composants abonnés au store reçoivent l'état mis à jour sous forme de props. Ils se re-rendent avec les nouvelles données.
Avantages du flux de données unidirectionnel
Prévisibilité : En imposant une direction unique pour le flux de données, Redux rend les changements d'état plus prévisibles et plus faciles à comprendre. Les actions indiquent explicitement quels changements se produisent, et les reducers définissent clairement comment les transitions d'état s'opèrent.
Débogage : Le flux de données unidirectionnel simplifie le débogage car vous pouvez tracer la propagation des changements d'état dans votre application. Les Redux DevTools améliorent encore cela en vous permettant de suivre les actions, d'inspecter les changements d'état au fil du temps et même de rejouer des actions pour reproduire des bugs.
Maintenabilité : Avec une séparation claire entre les données (état) et la logique (reducers), Redux favorise un code plus propre et plus facile à maintenir. Il réduit la probabilité de bugs causés par des mutations d'état incohérentes ou des effets de bord.
Scalabilité : À mesure que votre application gagne en taille et en complexité, le flux de données unidirectionnel aide à gérer les mises à jour d'état plus efficacement. Il évite les pièges de la liaison de données bidirectionnelle (two-way data binding) et garantit que les modifications de l'état sont contrôlées et gérables.
Tests : Puisque les reducers sont des fonctions pures qui prennent des entrées et produisent des sorties sans effets de bord, les tests unitaires deviennent simples. Vous pouvez tester les reducers avec différentes actions et scénarios d'état pour vous assurer qu'ils se comportent comme prévu.
Gestion d'état avec le Redux Store
La gestion d'état joue un rôle pivot dans le développement web moderne, garantissant que les applications conservent des états cohérents et prévisibles à travers divers composants.
Qu'est-ce que le Redux Store ?
Le Redux Store est le cœur de la gestion d'état Redux. Il contient l'arborescence complète de l'état de votre application. Le store vous permet de :
- Accéder à l'état actuel de votre application via
store.getState(). - Dispatcher des actions pour modifier l'état via
store.dispatch(action). - S'abonner aux changements de l'état afin que vos composants puissent se mettre à jour en conséquence via
store.subscribe(listener).
En substance, le Redux Store agit comme un référentiel centralisé pour l'état de votre application, facilitant un flux de données prévisible et rendant la gestion de l'état plus simple.
Structure du Store (State, Reducers, Actions)
Le state dans Redux représente l'état complet de votre application. Il est généralement structuré comme un objet JavaScript simple. La forme de l'état est définie par les reducers. Par exemple :
const initialState = {
todos: [],
visibilityFilter: 'SHOW_ALL',
};
Dans cet exemple, todos et visibilityFilter sont des morceaux d'état gérés par Redux.
Les Reducers sont des fonctions qui spécifient comment l'état de l'application change en réponse aux actions dispatchées au store. Ils prennent l'état actuel et une action comme arguments, et renvoient le nouvel état basé sur le type d'action.
Les reducers doivent être des fonctions pures, ce qui signifie qu'ils produisent la même sortie pour la même entrée et ne modifient pas directement l'état.
const todosReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo =>
(todo.id === action.id)
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
};
Dans cet exemple, todosReducer gère la partie todos de l'état, traitant des actions comme 'ADD_TODO' et 'TOGGLE_TODO' pour ajouter de nouveaux todos ou basculer leur état d'achèvement.
Les Actions sont des objets JavaScript simples qui décrivent ce qui s'est passé dans votre application. Elles sont la seule source d'information pour le store. Les actions ont généralement un champ type qui indique le type d'action effectuée, et elles peuvent également transporter des données supplémentaires nécessaires à l'action.
const addTodo = (text) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
const toggleTodo = (id) => ({
type: 'TOGGLE_TODO',
id
});
Dans cet exemple, addTodo et toggleTodo sont des fonctions action creator qui renvoient des actions pour ajouter un nouveau todo et basculer l'état d'achèvement d'un todo, respectivement.
La relation entre ces éléments dans Redux est cruciale pour gérer efficacement l'état de l'application :
- Les Actions décrivent les événements qui se produisent dans votre application.
- Les Reducers spécifient comment l'état de l'application change en réponse aux actions.
- Le Store détient l'état de l'application et vous permet de dispatcher des actions pour mettre à jour l'état.
Ensemble, ces composants forment la structure centrale de la gestion d'état Redux, offrant un moyen clair et prévisible de gérer et de mettre à jour l'état de l'application dans l'ensemble de votre projet.
Actions : Initier des changements d'état
Gérer l'état efficacement est au cœur de la création d'applications dynamiques et réactives. Les actions, au sein de l'architecture Redux et des bibliothèques de gestion d'état similaires, servent d'éléments importants pour initier les changements d'état.
Action Creators (Fonctions pour créer des actions)
Les action creators dans Redux sont des fonctions qui créent et renvoient des objets action. Ces objets action décrivent ce qui s'est passé dans votre application et sont dispatchés au Redux store pour initier des changements d'état.
Les action creators encapsulent la logique de création des actions, rendant votre code plus modulaire et plus facile à tester.
Voici un exemple d'action creator :
// Fonction action creator
const addTodo = (text) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
});
// Utilisation de l'action creator
const newTodoAction = addTodo('Buy groceries');
Dans cet exemple :
addTodoest une fonction action creator qui prendtextcomme paramètre et renvoie un objet action.- L'objet action possède un champ
type('ADD_TODO') qui identifie le type d'action et des champs supplémentaires (idettext) qui fournissent les données nécessaires à l'action.
Les action creators simplifient le processus de création d'actions, en particulier lorsque les actions nécessitent des données complexes ou des calculs avant d'être dispatchées.
Action Types (Identifier les différentes actions)
Les action types dans Redux sont des constantes de type chaîne de caractères qui définissent le type d'action effectuée. Ils sont utilisés pour identifier et différencier les différentes actions qui peuvent être dispatchées au Redux store. En utilisant des constantes pour les types d'actions, Redux garantit que les types d'actions sont uniques et faciles à référencer dans toute votre application.
Voici comment les action types sont généralement définis :
// Action types en tant que constantes
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
Ces constantes (ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER) représentent différentes actions qui peuvent survenir dans votre application, comme l'ajout d'un todo, le basculement de l'état d'achèvement d'un todo ou la définition d'un filtre de visibilité pour les todos.
Les action types sont généralement utilisés dans les objets action créés par les action creators et sont mis en correspondance dans les reducers pour déterminer comment l'état doit changer en réponse à chaque action.
// Exemple d'utilisation des action types dans un reducer
const todosReducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case TOGGLE_TODO:
return state.map(todo =>
(todo.id === action.id)
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
};
Dans cet exemple :
ADD_TODOetTOGGLE_TODOsont des action types utilisés dans letodosReducerpour gérer différents types d'actions ('ADD_TODO'et'TOGGLE_TODO').- Le champ
action.typedans l'instruction switch garantit que le reducer répond de manière appropriée à chaque action dispatchée en fonction de son type.
Comment traiter les changements d'état
Au cœur de la gestion d'état se trouvent les reducers, des fonctions pures conçues pour gérer les transitions d'état de manière contrôlée et immuable.
Fonctions pures : les Reducers au cœur du système
Les reducers dans Redux sont des fonctions pures chargées de spécifier comment l'état de l'application change en réponse aux actions dispatchées au store. Ils prennent l'état actuel et une action comme arguments, et renvoient le nouvel état basé sur le type d'action.
Voici une analyse du fonctionnement des reducers et de leur rôle dans la gestion des changements d'état :
Fonctions pures : Les reducers sont des fonctions pures, ce qui signifie qu'ils :
- Produisent la même sortie pour la même entrée chaque fois qu'ils sont appelés.
- Ne provoquent aucun effet de bord (comme la modification d'arguments ou de variables globales).
- Ne mutent pas l'état directement, mais renvoient à la place un nouvel objet d'état.
Gestion des transitions d'état : Les reducers spécifient comment l'état de l'application change en réponse à différents types d'actions. Ils utilisent l'état actuel et l'action dispatchée pour calculer et renvoyer le nouvel état.
// Exemple d'un reducer de todos
const todosReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo =>
(todo.id === action.id)
? { ...todo, completed: !todo.completed }
: todo
);
default:
return state;
}
};
Dans cet exemple :
todosReducerest une fonction pure qui prendstate(tableau de todos actuel) etactioncomme arguments.- Selon le
action.type, il calcule et renvoie un nouvel état (tableau de todos mis à jour).
Mises à jour immuables de l'état : Les reducers ne doivent jamais muter l'état directement. Au lieu de cela, ils créent des copies de l'état et modifient les copies pour produire un nouvel objet d'état. Cela garantit que Redux peut détecter les changements d'état et mettre à jour les composants efficacement.
Principe de responsabilité unique : Chaque reducer gère généralement les mises à jour d'une tranche (slice) spécifique de l'état de l'application. Cela aide à maintenir une séparation claire des préoccupations et rend les reducers plus faciles à comprendre, à tester et à maintenir.
Caractéristiques des fonctions pures
Les fonctions pures, y compris les reducers Redux, présentent des caractéristiques spécifiques qui les rendent bien adaptées à la gestion des changements d'état :
Déterministe : Une fonction pure produit toujours la même sortie pour la même entrée. Cette prévisibilité garantit que les reducers se comportent de manière cohérente et sont plus faciles à raisonner.
Pas d'effets de bord : Les fonctions pures ne modifient pas les arguments d'entrée ni aucun état externe. Elles dépendent uniquement de leurs paramètres d'entrée et produisent une sortie sans causer d'effets de bord observables.
Données immuables : Les fonctions pures ne mutent pas les données. Au lieu de cela, elles créent et renvoient de nouvelles structures de données. Dans Redux, les reducers produisent un nouvel objet d'état sans modifier l'état existant, permettant une détection efficace des changements et une gestion d'état performante.
Transparence référentielle : Les fonctions pures peuvent être remplacées par leurs valeurs de retour sans affecter l'exactitude du programme. Cette propriété favorise la composabilité et facilite le test et le raisonnement du code.
Anatomie d'une fonction Reducer
Une fonction reducer définit, à la base, comment l'état de l'application change en réponse aux actions dispatchées. Cette fonction prend deux paramètres : l'état actuel et un objet action, déterminant le nouvel état en fonction du type d'action reçu.
Paramètres : État précédent et objet Action
Une fonction reducer dans Redux est une fonction pure qui prend deux paramètres : l'état précédent (l'état avant que l'action ne soit appliquée) et un objet action. Ces paramètres définissent comment le reducer calcule le prochain état de l'application.
État précédent : Ce paramètre représente l'état actuel de l'application avant que l'action ne soit dispatchée. Il est immuable et ne doit pas être modifié directement au sein du reducer.
Objet Action : Un objet action est un objet JavaScript simple qui décrit ce qui s'est passé dans votre application. Il possède généralement un champ type qui indique le type d'action effectuée. D'autres champs dans l'objet action peuvent fournir des données supplémentaires nécessaires à la mise à jour de l'état.
const action = {
type: 'ADD_TODO',
id: 1,
text: 'Buy groceries'
};
Dans cet exemple, action.type est 'ADD_TODO', indiquant que nous voulons ajouter un nouvel élément todo à l'état.
Valeur de retour : État mis à jour
La fonction reducer doit renvoyer l'état mis à jour en fonction de l'état précédent et de l'objet action qui lui est transmis. L'état mis à jour est généralement un nouvel objet qui représente l'état de l'application après l'application de l'action.
Voici la structure de base d'une fonction reducer :
const initialState = {
todos: [],
visibilityFilter: 'SHOW_ALL'
};
const todoAppReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: action.id,
text: action.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
(todo.id === action.id)
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_VISIBILITY_FILTER':
return {
...state,
visibilityFilter: action.filter
};
default:
return state;
}
};
Dans cet exemple :
todoAppReducerest une fonction reducer qui gère l'état des todos et des filtres de visibilité.- Elle prend
state(état précédent) etactioncomme paramètres. - Selon le
action.type, elle calcule et renvoie un nouvel objet d'état qui reflète les changements causés par l'action.
Points clés :
Mise à jour immuable : Les reducers ne doivent jamais modifier directement l'état précédent. Au lieu de cela, ils créent un nouvel objet d'état en copiant l'état précédent (...state) et en lui appliquant des modifications.
Cas par défaut : Le cas default dans l'instruction switch renvoie l'état actuel inchangé si le reducer ne reconnaît pas le type d'action. Cela garantit que le reducer renvoie toujours un objet d'état valide, même si aucune modification n'est apportée.
Responsabilité unique : Chaque cas dans l'instruction switch correspond à un type d'action spécifique et est responsable de la mise à jour d'une tranche spécifique de l'état de l'application. Cela favorise une séparation claire des préoccupations et rend les reducers plus faciles à comprendre et à maintenir.
Comment gérer différentes actions dans les Reducers
Dans Redux, vous pouvez gérer différentes actions dans les reducers en utilisant soit des instructions switch, soit une logique conditionnelle. Les deux approches visent à déterminer comment l'état de l'application doit changer en fonction du type d'action dispatché.
Utilisation des instructions Switch
Les instructions switch sont couramment utilisées dans les reducers Redux pour gérer différents types d'actions. Chaque case dans l'instruction switch correspond à un type d'action spécifique, et le reducer exécute la logique correspondante en fonction du type d'action.
Voici un exemple d'utilisation d'instructions switch dans un reducer :
const initialState = {
todos: [],
visibilityFilter: 'SHOW_ALL'
};
const todoAppReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: action.id,
text: action.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
(todo.id === action.id)
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_VISIBILITY_FILTER':
return {
...state,
visibilityFilter: action.filter
};
default:
return state;
}
};
Dans cet exemple :
- La fonction
todoAppReducerutilise une instruction switch pour gérer différents types d'actions ('ADD_TODO','TOGGLE_TODO','SET_VISIBILITY_FILTER'). - Chaque bloc
casespécifie comment l'état doit être mis à jour en réponse au type d'action correspondant. - Le cas
defaultrenvoie l'état actuel inchangé si le reducer ne reconnaît pas le type d'action, garantissant que le reducer renvoie toujours un objet d'état valide.
Utilisation de la logique conditionnelle
Alternativement, les reducers peuvent également utiliser une logique conditionnelle (instructions if-else) pour déterminer comment mettre à jour l'état en fonction du type d'action. Bien que moins courante que les instructions switch dans Redux, la logique conditionnelle peut être utilisée de manière similaire pour gérer les actions.
Voici un exemple d'utilisation de la logique conditionnelle dans un reducer :
const todoAppReducer = (state = initialState, action) => {
if (action.type === 'ADD_TODO') {
return {
...state,
todos: [
...state.todos,
{
id: action.id,
text: action.text,
completed: false
}
]
};
} else if (action.type === 'TOGGLE_TODO') {
return {
...state,
todos: state.todos.map(todo =>
(todo.id === action.id)
? { ...todo, completed: !todo.completed }
: todo
)
};
} else if (action.type === 'SET_VISIBILITY_FILTER') {
return {
...state,
visibilityFilter: action.filter
};
} else {
return state;
}
};
Dans cet exemple :
- La fonction
todoAppReducerutilise des instructions if-else pour vérifier le type d'action (action.type) et exécuter une logique différente selon le type d'action. - Chaque condition spécifie comment l'état doit être mis à jour pour le type d'action correspondant.
- Le bloc
elsefinal renvoie l'état actuel inchangé si le type d'action n'est pas reconnu.
Choisir entre les instructions Switch et la logique conditionnelle
1. Instructions Switch :
- Avantages : Les instructions switch sont généralement plus lisibles et plus faciles à maintenir lors de la gestion de multiples types d'actions dans les reducers Redux. Elles séparent clairement les différents cas basés sur les types d'actions.
- Considérations : Assurez-vous que chaque type d'action possède un
casecorrespondant dans l'instruction switch pour gérer correctement les mises à jour.
2. Logique conditionnelle :
- Avantages : La logique conditionnelle (instructions if-else) offre de la flexibilité et peut être plus facile à comprendre dans certains scénarios où il y a peu de types d'actions.
- Considérations : Maintenez la cohérence dans la gestion des types d'actions et assurez-vous que chaque condition gère correctement les mises à jour d'état.
En pratique, les instructions switch sont l'approche recommandée dans les reducers Redux en raison de leur clarté et de la convention au sein de la communauté Redux. Elles aident à maintenir une approche structurée de la gestion des changements d'état basés sur différents types d'actions, favorisant la cohérence et la prévisibilité dans les applications Redux.
Dispatcher des actions : Comment mettre à jour le Redux Store
Dispatcher des actions dans Redux est fondamental pour gérer les mises à jour d'état au sein de votre application. Redux, un conteneur d'état prévisible pour les applications JavaScript, s'appuie sur les actions comme vecteurs d'information qui envoient des données de votre application vers le Redux store.
La fonction dispatch
Dans Redux, la fonction dispatch est une méthode fournie par le Redux store. Elle est utilisée pour dispatcher des actions afin de déclencher des changements d'état dans l'application. Lorsqu'une action est dispatchée, le Redux store appelle la fonction reducer qui lui est associée, calcule le nouvel état et informe tous les abonnés que l'état a été mis à jour.
Voici comment utiliser la fonction dispatch :
import { createStore } from 'redux';
// Fonction reducer
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
// Créer le Redux store
const store = createStore(counterReducer);
// Dispatcher des actions pour mettre à jour l'état
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
Dans cet exemple :
- Nous créons un Redux store en utilisant
createStoreet en passant la fonctioncounterReducer. - La fonction
store.dispatchest utilisée pour dispatcher des actions ({ type: 'INCREMENT' }et{ type: 'DECREMENT' }) afin de mettre à jour l'état. - Chaque action dispatchée déclenche le cas correspondant dans le reducer, mettant à jour l'état comme défini.
Dispatcher des actions depuis des composants ou des événements
Dans une application Redux typique, les actions sont souvent dispatchées depuis des composants React en réponse aux interactions des utilisateurs ou à d'autres événements.
Pour dispatcher des actions depuis des composants, vous connectez généralement le composant au Redux store en utilisant la fonction connect de React Redux ou des hooks comme useDispatch.
Voici comment vous pouvez dispatcher des actions depuis un composant React en utilisant connect et mapDispatchToProps :
import React from 'react';
import { connect } from 'react-redux';
// Fonctions action creator
const increment = () => ({ type: 'INCREMENT' });
const decrement = () => ({ type: 'DECREMENT' });
// Définition du composant
const Counter = ({ count, increment, decrement }) => (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
// Mappage de l'état aux props
const mapStateToProps = (state) => ({
count: state.count
});
// Mappage du dispatch aux props
const mapDispatchToProps = {
increment,
decrement
};
// Connecter le composant au Redux store
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Dans cet exemple :
incrementetdecrementsont des fonctions action creator qui renvoient des actions ({ type: 'INCREMENT' }et{ type: 'DECREMENT' }).- Le composant
Counterest connecté au Redux store viaconnect. Il reçoitcountde l'état Redux en tant que prop, ainsi que les action creatorsincrementetdecrement. - Cliquer sur les boutons "Increment" et "Decrement" dispatche des actions, qui sont traitées par le reducer pour mettre à jour l'état Redux.
Alternativement, vous pouvez utiliser les hooks de React Redux (useDispatch) pour dispatcher des actions dans des composants fonctionnels :
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
const Counter = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
const handleIncrement = () => {
dispatch({ type: 'INCREMENT' });
};
const handleDecrement = () => {
dispatch({ type: 'DECREMENT' });
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
};
export default Counter;
Dans cet exemple de composant fonctionnel :
useSelectorest utilisé pour sélectionnercountdepuis l'état du Redux store.useDispatchest utilisé pour obtenir la fonctiondispatchdu Redux store.- Les fonctions
handleIncrementethandleDecrementdispatchent des actions ({ type: 'INCREMENT' }et{ type: 'DECREMENT' }) pour mettre à jour l'état Redux lorsque les boutons sont cliqués.
Comment accéder à des données spécifiques du Store
Accéder à des données spécifiques du store dans Redux implique de naviguer à travers la structure de l'état de l'application pour récupérer les informations précises nécessaires au rendu des composants ou à l'exécution d'une logique.
Créer des fonctions Selector
Les selectors dans Redux sont des fonctions qui encapsulent la logique de récupération de morceaux spécifiques de données à partir de l'état du Redux store. Ils aident à découpler les composants de la structure de l'état et facilitent l'accès et la transformation efficaces des données.
Voici comment vous pouvez créer des fonctions selector :
// Exemple d'état Redux
const initialState = {
todos: [
{ id: 1, text: 'Learn Redux', completed: false },
{ id: 2, text: 'Write Redux selectors', completed: true },
// plus de todos...
],
visibilityFilter: 'SHOW_COMPLETED'
};
// Fonction selector pour obtenir les todos de l'état
const getTodos = (state) => state.todos;
// Fonction selector pour filtrer les todos en fonction du filtre de visibilité
const getVisibleTodos = (state) => {
const todos = getTodos(state);
const visibilityFilter = state.visibilityFilter;
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed);
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed);
case 'SHOW_ALL':
default:
return todos;
}
};
Dans cet exemple :
getTodosest une fonction selector qui récupère le tableautodosdepuis l'état Redux.getVisibleTodosest une fonction selector qui filtre lestodosen fonction duvisibilityFilterstocké dans l'état.
Les selectors peuvent également être composés pour créer des selectors plus complexes :
// Fonction selector composée pour obtenir les todos visibles
const getVisibleTodos = (state) => {
const todos = getTodos(state);
const visibilityFilter = state.visibilityFilter;
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return getCompletedTodos(todos);
case 'SHOW_ACTIVE':
return getActiveTodos(todos);
case 'SHOW_ALL':
default:
return todos;
}
};
// Fonctions d'aide pour filtrer les todos
const getCompletedTodos = (todos) => todos.filter(todo => todo.completed);
const getActiveTodos = (todos) => todos.filter(todo => !todo.completed);
Mémoïsation pour une utilisation efficace des Selectors
La mémoïsation est une technique utilisée pour optimiser les calculs coûteux en mettant en cache les résultats des appels de fonction basés sur leurs entrées. Dans le contexte des selectors Redux, la mémoïsation peut améliorer les performances en garantissant que les selectors ne recalculent leurs résultats que lorsque leur entrée (l'état) change.
Vous pouvez utiliser des bibliothèques comme reselect pour la mémoïsation dans les selectors Redux :
npm install reselect
Exemple d'utilisation de reselect pour la mémoïsation :
import { createSelector } from 'reselect';
// Selectors
const getTodos = (state) => state.todos;
const getVisibilityFilter = (state) => state.visibilityFilter;
// Selector mémoïsé pour obtenir les todos visibles
const getVisibleTodos = createSelector(
[getTodos, getVisibilityFilter],
(todos, visibilityFilter) => {
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed);
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed);
case 'SHOW_ALL':
default:
return todos;
}
}
);
Dans cet exemple :
createSelectordereselectcrée un selector mémoïsé qui prendgetTodosetgetVisibilityFiltercomme selectors d'entrée.- La fonction selector calcule les todos filtrés en fonction du
visibilityFilteret met en cache le résultat jusqu'à ce que les selectors d'entrée changent.
Comment connecter des composants React à Redux
Connecter des composants React à Redux est une technique fondamentale pour gérer efficacement l'état de l'application dans les projets basés sur React. Redux sert de store centralisé qui contient l'état complet de votre application, le rendant accessible à tout composant qui en a besoin.
La fonction connect de la bibliothèque react-redux
Dans les applications React utilisant Redux pour la gestion d'état, la fonction connect de la bibliothèque react-redux est utilisée pour connecter les composants React au Redux store. Elle fournit un moyen d'injecter l'état Redux et les fonctions de dispatch d'actions (dispatchers) dans vos composants.
Voici comment utiliser connect :
import React from 'react';
import { connect } from 'react-redux';
// Définir un composant React
const Counter = ({ count, increment, decrement }) => (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
// Mapper l'état Redux aux props du composant
const mapStateToProps = (state) => ({
count: state.count
});
// Mapper les actions de dispatch aux props du composant
const mapDispatchToProps = {
increment: () => ({ type: 'INCREMENT' }),
decrement: () => ({ type: 'DECREMENT' })
};
// Connecter le composant au Redux store
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Mappage du State et du Dispatch aux Props
mapStateToProps : Cette fonction mappe l'état du Redux store aux props de votre composant React. Elle prend l'état Redux comme argument et renvoie un objet. Chaque champ de l'objet renvoyé deviendra une prop pour le composant connecté.
mapDispatchToProps : Cette fonction mappe les actions de dispatch aux props de votre composant React. Il peut s'agir d'un objet où chaque champ est une fonction action creator, ou d'une fonction qui reçoit dispatch comme argument et renvoie un objet. Chaque action creator sera automatiquement enveloppé par dispatch afin qu'ils puissent être appelés directement.
Dans l'exemple :
mapStateToPropsmappe le champcountde l'état Redux (state.count) à la propcountdu composantCounter.mapDispatchToPropsmappe les actionsincrementetdecrementaux props, de sorte que cliquer sur les boutons dans le composantCounterdispatchera les actions correspondantes ({ type: 'INCREMENT' }et{ type: 'DECREMENT' }).
Utiliser des composants connectés dans votre application
Une fois qu'un composant est connecté au Redux store via connect, il peut accéder à l'état Redux et dispatcher des actions via les props. Voici comment vous pouvez utiliser des composants connectés dans votre application :
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers'; // Importez votre reducer racine
import App from './App'; // Importez votre composant connecté
// Créer le Redux store avec le reducer racine
const store = createStore(rootReducer);
// Rendre le composant App à l'intérieur du Provider
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Dans cette configuration :
Providerest un composant dereact-reduxqui rend le Redux store disponible pour tous les composants imbriqués qui ont été connectés viaconnect.storeest créé aveccreateStoreet combiné avec un reducer racine (rootReducer) qui regroupe tous vos reducers en un seul.
En enveloppant votre composant de plus haut niveau (App dans ce cas) avec Provider et en passant le Redux store comme prop, tous les composants connectés au sein de votre application peuvent accéder au Redux store et interagir avec lui via les props (mappages mapStateToProps et mapDispatchToProps).
Techniques avancées de flux de données Redux
Les techniques avancées de flux de données Redux s'appuient sur les principes fondamentaux de la gestion d'état dans les applications complexes. Ces techniques vont au-delà des actions et reducers de base, introduisant des concepts tels que les middlewares, les selectors et les actions asynchrones.
Actions asynchrones (Redux Thunk, Redux Saga)
Dans Redux, la gestion des actions asynchrones implique de gérer des actions qui ont des effets de bord, comme la récupération de données depuis un serveur ou la mise à jour asynchrone de l'état. Redux propose plusieurs solutions de middleware pour gérer efficacement les actions asynchrones.
Redux Thunk
Redux Thunk est un middleware qui vous permet d'écrire des action creators qui renvoient une fonction au lieu d'un objet action. Cette fonction peut ensuite effectuer des opérations asynchrones et dispatcher des actions synchrones régulières lorsque les opérations asynchrones sont terminées.
Exemple d'utilisation de Redux Thunk pour des actions asynchrones :
Configuration du middleware Redux Thunk :
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers'; // Importez votre reducer racine
// Créer le Redux store avec le middleware thunk
const store = createStore(rootReducer, applyMiddleware(thunk));
Action Creator asynchrone utilisant Redux Thunk :
// Fonction action creator utilisant Redux Thunk
const fetchPosts = () => {
return async (dispatch) => {
dispatch({ type: 'FETCH_POSTS_REQUEST' });
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
dispatch({ type: 'FETCH_POSTS_SUCCESS', payload: posts });
} catch (error) {
dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message });
}
};
};
Dans cet exemple :
fetchPostsest un action creator qui renvoie une fonction au lieu d'un objet action.- À l'intérieur de la fonction, vous pouvez effectuer des opérations asynchrones (comme la récupération de données) et dispatcher des actions en fonction du résultat.
- Le middleware Redux Thunk intercepte les fonctions renvoyées par les action creators, permettant ainsi les actions asynchrones dans Redux.
Redux Saga
Redux Saga est un autre middleware pour gérer les effets de bord dans les applications Redux. Il utilise les générateurs ES6 pour rendre le code asynchrone plus facile à lire, à écrire et à tester.
Exemple d'utilisation de Redux Saga pour gérer des actions asynchrones :
Configuration du middleware Redux Saga :
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers'; // Importez votre reducer racine
import rootSaga from './sagas'; // Importez votre saga racine
// Créer le middleware Redux Saga
const sagaMiddleware = createSagaMiddleware();
// Créer le Redux store avec le middleware Saga
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
// Exécuter la saga racine
sagaMiddleware.run(rootSaga);
Exemple de Saga (rootSaga.js) :
import { all, call, put, takeEvery } from 'redux-saga/effects';
import { fetchPostsSuccess, fetchPostsFailure } from './actions'; // Importez vos action creators
// Worker saga pour récupérer les posts
function* fetchPostsSaga() {
try {
const response = yield call(fetch, 'https://jsonplaceholder.typicode.com/posts');
const posts = yield call([response, 'json']);
yield put(fetchPostsSuccess(posts));
} catch (error) {
yield put(fetchPostsFailure(error.message));
}
}
// Watcher saga pour écouter l'action FETCH_POSTS_REQUEST
function* watchFetchPosts() {
yield takeEvery('FETCH_POSTS_REQUEST', fetchPostsSaga);
}
// Saga racine
export default function* rootSaga() {
yield all([
watchFetchPosts()
// Ajoutez d'autres watchers si nécessaire
]);
}
Dans cet exemple :
fetchPostsSagaest une saga worker qui effectue l'opération asynchrone (récupération des posts).watchFetchPostsest une saga watcher qui écoute des actions spécifiques (FETCH_POSTS_REQUEST) et déclenche la saga worker correspondante.rootSagacombine plusieurs sagas en utilisantallet les exécute viasagaMiddleware.run.
Middleware pour étendre les fonctionnalités de Redux
Le middleware dans Redux offre un moyen d'étendre les capacités du Redux store, telles que la journalisation des actions, la gestion des opérations asynchrones, le routage, et plus encore. Le middleware se situe entre le dispatch d'une action et le moment où elle atteint le reducer, permettant l'interception et la manipulation des actions.
Exemple de middleware personnalisé :
const loggerMiddleware = store => next => action => {
console.log('Dispatching action:', action);
const result = next(action);
console.log('New state:', store.getState());
return result;
};
// Application du middleware personnalisé au Redux store
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers'; // Importez votre reducer racine
// Créer le Redux store avec le middleware personnalisé
const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
Dans cet exemple :
loggerMiddlewareest une fonction middleware personnalisée qui journalise chaque action dispatchée et l'état résultant.nextest une fonction fournie par Redux qui permet à l'action de continuer vers le middleware suivant ou le reducer.- Le middleware personnalisé améliore les fonctionnalités de Redux en interceptant les actions, en exécutant une logique personnalisée et, éventuellement, en dispatchant de nouvelles actions ou en modifiant celles existantes.
Bonnes pratiques pour gérer le flux de données dans Redux
Redux offre un moyen structuré de gérer l'état dans les applications JavaScript, mais une utilisation efficace nécessite de respecter certaines bonnes pratiques. Voici mes recommandations clés pour gérer le flux de données dans Redux :
Organiser les Reducers et les Actions
Structure et organisation des fichiers :
- Séparation des préoccupations : Gardez les actions, les reducers et les selectors dans des fichiers séparés pour maintenir la clarté et la modularité.
- Structure basée sur les fonctionnalités : Regroupez les actions et reducers liés ensemble en fonction des fonctionnalités plutôt que des types.
src/
├── actions/
│ ├── todosActions.js
│ └── userActions.js
├── reducers/
│ ├── todosReducer.js
│ └── userReducer.js
├── selectors/
│ ├── todosSelectors.js
│ └── userSelectors.js
└── store.js
Action Types :
- Constantes : Utilisez des constantes ou des enums pour les types d'actions afin d'éviter les fautes de frappe et d'assurer la cohérence.
// Action types
export const ADD_TODO = 'ADD_TODO';
export const DELETE_TODO = 'DELETE_TODO';
Composition des Reducers :
- Combiner les reducers : Utilisez
combineReducersde Redux pour combiner plusieurs reducers en un seul reducer racine.
import { combineReducers } from 'redux';
import todosReducer from './todosReducer';
import userReducer from './userReducer';
const rootReducer = combineReducers({
todos: todosReducer,
user: userReducer
});
export default rootReducer;
Mises à jour immuables de l'état
Immuabilité avec l'opérateur Spread :
- Utilisez l'opérateur spread (
...) : Créez de nouveaux objets ou tableaux lors de la mise à jour de l'état pour maintenir l'immuabilité.
// Mise à jour d'un tableau dans l'état Redux
const todosReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [
...state.todos,
{
id: action.id,
text: action.text,
completed: false
}
]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
(todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo
)
};
default:
return state;
}
};
Bibliothèques d'immuabilité :
- Immutable.js : Envisagez d'utiliser des bibliothèques comme Immutable.js pour des structures de données plus complexes afin d'imposer l'immuabilité et d'optimiser les performances.
import { Map, List } from 'immutable';
const initialState = Map({
todos: List(),
user: Map()
});
const todosReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return state.update('todos', todos => todos.push(Map({
id: action.id,
text: action.text,
completed: false
})));
case TOGGLE_TODO:
return state.update('todos', todos =>
todos.map(todo =>
(todo.get('id') === action.id) ? todo.set('completed', !todo.get('completed')) : todo
)
);
default:
return state;
}
};
Tester les applications Redux
Tests unitaires :
- Reducers : Testez les reducers pour vous assurer qu'ils gèrent correctement les actions et renvoient l'état attendu.
describe('todosReducer', () => {
it('should handle ADD_TODO', () => {
const action = { type: 'ADD_TODO', id: 1, text: 'Test todo' };
const initialState = { todos: [] };
const expectedState = { todos: [{ id: 1, text: 'Test todo', completed: false }] };
expect(todosReducer(initialState, action)).toEqual(expectedState);
});
});
Tests d'intégration :
- Action Creators et Thunks : Testez les action creators et les thunks pour vérifier qu'ils dispatchent les bonnes actions ou gèrent correctement les opérations asynchrones.
describe('fetchPosts action creator', () => {
it('creates FETCH_POSTS_SUCCESS when fetching posts has been done', () => {
const expectedActions = [
{ type: 'FETCH_POSTS_REQUEST' },
{ type: 'FETCH_POSTS_SUCCESS', payload: { /* mocked data */ } }
];
const store = mockStore({ posts: [] });
return store.dispatch(fetchPosts()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
Intégration avec les composants :
- Composants connectés : Testez les composants connectés en utilisant
redux-mock-storepour simuler le comportement du Redux store.
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { render } from '@testing-library/react';
import App from './App';
const mockStore = configureStore([]);
describe('<App />', () => {
it('renders App component', () => {
const store = mockStore({ /* mocked state */ });
const { getByText } = render(
<Provider store={store}>
<App />
</Provider>
);
expect(getByText('Welcome to Redux App')).toBeInTheDocument();
});
});
Conclusion
Redux offre une solution de gestion d'état puissante pour les applications JavaScript, fournissant un moyen prévisible et centralisé de gérer l'état de l'application.
Qu'il s'agisse de gérer des opérations asynchrones avec des middlewares comme Redux Thunk ou Redux Saga, ou d'optimiser la gestion de l'état par des pratiques de données immuables, Redux vous permet de construire des applications scalables et maintenables.
En maîtrisant ces techniques, vous pouvez tirer parti de Redux pour rationaliser le flux de données, améliorer les performances de l'application et simplifier les complexités de la gestion d'état dans le développement web moderne.
C'est tout pour cet article ! Si vous souhaitez poursuivre la conversation ou si vous avez des questions, des suggestions ou des commentaires, n'hésitez pas à me contacter sur LinkedIn. Et si vous avez apprécié ce contenu, envisagez de m'offrir un café pour soutenir la création de plus de contenus adaptés aux développeurs.