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

  1. Qu'est-ce que Redux ?
  2. Pourquoi utiliser Redux pour la gestion des données ?
  3. Concepts fondamentaux du flux de données Redux
  4. Flux de données unidirectionnel
  5. Avantages du flux de données unidirectionnel
  6. Gestion d'état avec le Redux Store
  7. Qu'est-ce que le Redux Store ?
  8. Structure du Store (State, Reducers, Actions)
  9. Actions : Initier des changements d'état
  10. Action Creators (Fonctions pour créer des actions)
  11. Action Types (Identifier les différentes actions)
  12. Comment traiter les changements d'état
  13. Fonctions pures : les Reducers au cœur du système
  14. Caractéristiques des fonctions pures
  15. Anatomie d'une fonction Reducer
  16. Paramètres : État précédent et objet Action
  17. Valeur de retour : État mis à jour
  18. Comment gérer différentes actions dans les Reducers
  19. Utilisation des instructions Switch ou de la logique conditionnelle
  20. Dispatcher des actions : Comment mettre à jour le Redux Store
  21. La fonction dispatch
  22. Dispatcher des actions depuis des composants ou des événements
  23. Comment accéder à des données spécifiques du Store
  24. Créer des fonctions Selector
  25. Mémoïsation pour une utilisation efficace des Selectors
  26. Comment connecter des composants React à Redux
  27. La fonction connect de la bibliothèque react-redux
  28. Mappage du State et du Dispatch aux Props
  29. Utiliser des composants connectés dans votre application
  30. Techniques avancées de flux de données Redux
  31. Actions asynchrones (Redux Thunk, Redux Saga)
  32. Middleware pour étendre les fonctionnalités de Redux
  33. Bonnes pratiques pour gérer le flux de données dans Redux
  34. 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 :

  1. 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.
  2. 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.
  3. Store : Le store contient l'état de l'application. Il permet d'accéder à l'état via getState(), de mettre à jour l'état via dispatch(action) et d'enregistrer des écouteurs via subscribe(listener).
  4. 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 :

  1. 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 champ type qui décrit le type d'action effectuée.
  2. 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.
  3. 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.
  4. 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 :

  • addTodo est une fonction action creator qui prend text comme 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 (id et text) 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_TODO et TOGGLE_TODO sont des action types utilisés dans le todosReducer pour gérer différents types d'actions ('ADD_TODO' et 'TOGGLE_TODO').
  • Le champ action.type dans 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 :

  • todosReducer est une fonction pure qui prend state (tableau de todos actuel) et action comme 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 :

  • todoAppReducer est une fonction reducer qui gère l'état des todos et des filtres de visibilité.
  • Elle prend state (état précédent) et action comme 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 todoAppReducer utilise une instruction switch pour gérer différents types d'actions ('ADD_TODO', 'TOGGLE_TODO', 'SET_VISIBILITY_FILTER').
  • Chaque bloc case spécifie comment l'état doit être mis à jour en réponse au type d'action correspondant.
  • Le cas default renvoie 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 todoAppReducer utilise 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 else final 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 case correspondant 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 createStore et en passant la fonction counterReducer.
  • La fonction store.dispatch est 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 :

  • increment et decrement sont des fonctions action creator qui renvoient des actions ({ type: 'INCREMENT' } et { type: 'DECREMENT' }).
  • Le composant Counter est connecté au Redux store via connect. Il reçoit count de l'état Redux en tant que prop, ainsi que les action creators increment et decrement.
  • 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 :

  • useSelector est utilisé pour sélectionner count depuis l'état du Redux store.
  • useDispatch est utilisé pour obtenir la fonction dispatch du Redux store.
  • Les fonctions handleIncrement et handleDecrement dispatchent 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 :

  • getTodos est une fonction selector qui récupère le tableau todos depuis l'état Redux.
  • getVisibleTodos est une fonction selector qui filtre les todos en fonction du visibilityFilter stocké 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 :

  • createSelector de reselect crée un selector mémoïsé qui prend getTodos et getVisibilityFilter comme selectors d'entrée.
  • La fonction selector calcule les todos filtrés en fonction du visibilityFilter et 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 :

  • mapStateToProps mappe le champ count de l'état Redux (state.count) à la prop count du composant Counter.
  • mapDispatchToProps mappe les actions increment et decrement aux props, de sorte que cliquer sur les boutons dans le composant Counter dispatchera 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 :

  • Provider est un composant de react-redux qui rend le Redux store disponible pour tous les composants imbriqués qui ont été connectés via connect.
  • store est créé avec createStore et 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 :

  • fetchPosts est 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 :

  • fetchPostsSaga est une saga worker qui effectue l'opération asynchrone (récupération des posts).
  • watchFetchPosts est une saga watcher qui écoute des actions spécifiques (FETCH_POSTS_REQUEST) et déclenche la saga worker correspondante.
  • rootSaga combine plusieurs sagas en utilisant all et les exécute via sagaMiddleware.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 :

  • loggerMiddleware est une fonction middleware personnalisée qui journalise chaque action dispatchée et l'état résultant.
  • next est 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 combineReducers de 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-store pour 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.