Redux
Redux est une bibliothèque de gestion d'état pour les applications JavaScript. Il centralise l'état de l'application dans un store unique, ce qui permet une gestion plus prédictible et structurée des changements d'état.
Store contient l'état global de l'application. Actions sont des objets décrivant ce qui doit se passer (types d'actions). Reducers déterminent comment l'état change en réponse aux actions.
L'utilisation de Redux est recommandée dans des applications complexes avec beaucoup de données partagées entre différents composants. Il permet de meilleure maintenir et de faciliter le debugage (avec DevTools), et garantit que l'état soit accessible de manière centralisée.
Reducer et concept de pur function
Un reducer est une fonction qui prend l'état actuel et une action en entrée, et retourne un nouvel état. Le reducer doit être des fonctions pures, c'est-à-dire qu'il doit toujours retourner le même résultat pour une même entrée sans provoquer d'effets secondaires. C'est-à-dire qu'il ne modifie pas directement l'état actuel (avec l'ancien redux, on doit retourner une nouvelle copie). il ne déclenche pas d'actions asynchrones ou d'actions en dehors de leur portée (comme des appels API ou la modification de données externes).
L'importance de la pureté d'un reducer réside dans la prévisibilité du comportement de l'application. Cela garantit que la gestion de l'état est contrôlable et déboguable.
Middleware
Un middleware est une fonction qui intercepte les actions avant qu'elles n'atteignent le reducer. Il est principalement utilisé pour gérer les tâches asynchrones, ou encore pour ajouter des fonctionnalités comme la gestion des erreurs ou journalisation.
Le middleware se situe entre le dispatch d'une action et le reducer
Exemple populaire : redux-thunk permet de gérer des actions asynchrones en envoyant des fonctions (au lieu d'actions) qui dispatchent des actions à un moment opportun (par exemple, après un appel API réussi).
Effets secondaire asynchrone
Pour gérer les effets asynchrones dans Redux, il existe plusieurs approches : redux-thunk : Permet de créer des actions qui retournent des fonctions (plutôt que des objets). Ces fonctions peuvent alors exécuter des opérations asynchrones et dispatch des actions une fois les opérations terminées.
redux-saga : Utilise des generators pour gérer des flux asynchrones complexes et permettre un meilleur contrôle sur les effets secondaires. Il offre une approche plus modulaire et testable par rapport à redux-thunk.
redux-observable : Utilise des observables via RxJS pour gérer les flux asynchrones. Il permet de composer des flux complexes en réponse aux actions.
CombineReducers Vs reducer
combineReducers est une fonction utilitaire qui permet de combiner plusieurs reducers en un seul, où chaque reducer est responsable d'une partie spécifique de l'état global. Avec combineReducers, chaque reducer reçoit uniquement une partie de l'état qui lui est assignée, ce qui permet de mieux organiser le code et de le rendre plus maintenable. Cela suit le principe de séparation des préoccupations.
Seul reducer
Si on gère tout l'état global avec un seul reducer, notre reducer peut devenir trop complexe à maintenir, ce qui peut rendre le code moins lisible.
Optimisation redux
Plusieurs stratégies peuvent être utilisées pour optimiser les performances avec Redux :
Normalisation de l'état On structure l'état comme une base de données avec des tables liées par des ID pour éviter les duplications et faciliter la mise à jour des données sans recréer d'objets profondément imbriqués.
Mémorisation avec reselect : On utilise des sélecteurs mémorisés pour éviter de recalculer des données si les parties pertinentes de l'état n'ont pas changé.
Immutable Updates : on assure de ne pas modifier directement l'état dans les reducers, mais de créer une nouvelle copie. Des outils comme Immer peuvent faciliter cette approche en automatisant une partie des manipulations immuables.
Split des reducers et du store : on utilise le code splitting pour charger dynamiquement les reducers uniquement quand ils sont nécessaires, par exemple dans des applications très grandes avec beaucoup de modules.
Immuabilité redux
L'immuabilité garantit que l'état ne peut pas être modifié directement. Chaque mise à jour de l'état retourne un nouvel objet sans affecter l'ancien. L'avantage est de :
Débogage simplifié : Comme l'état précédent n'est pas modifié, il est possible de suivre facilement l'historique des modifications d'état. Time-travel debugging : Grâce à l'immuabilité, vous pouvez revenir à un état précédent de l'application. Optimisations de performance : Les bibliothèques comme React utilisent des comparaisons d'objets pour décider si un composant doit être re-rendu. Si l'état est immuable, il est facile de comparer deux objets par référence.
Mise à jour objets dans store
Lorsqu' on met à jour des objets, il est important de ne pas modifier directement les objets existants. Au lieu de cela, on doit créer une nouvelle copie des objets affectés tout en conservant les autres intacts. Cela peut être fait via l'opérateur spread (...) ou avec des outils comme Immer.Avec redux RTK, c'est déjà gérer par le framework
Limite de redux
Les inconvénients de redux, je trouve au niveau de:
Boilerplate : Redux peut impliquer une grande quantité de code répétitif pour définir des actions, reducers, et types d'actions, en particulier dans les grandes applications.
Complexité : Pour les petites applications ou des cas simples, l'utilisation de Redux peut être excessive. La gestion de l'état via le useState et useReducer de React est souvent suffisante.
Approche stricte : Redux impose une approche unidirectionnelle stricte, ce qui peut rendre la gestion d'états complexes (comme les formulaires dynamiques ou les opérations multi-étapes) plus difficile à implémenter que d'autres solutions.
On peut envisager d'autres solutions comme :
Context API de React : Pour des cas d'état plus simples ou limités à une portion spécifique de l'application.
Recoil ou Zustand : Des solutions plus légères qui permettent une gestion d'état plus flexible sans les contraintes structurelles de Redux.
Tester des actions et reducers
Pour tester les actions et reducers est simple car ce sont des fonctions pures (pas d'effets secondaires). Voici les étapes pour tester les actions et reducers :
Tester les actions : Les actions sont simplement des objets ou des fonctions qui retournent des objets (dans le cas de thunk). On peut les tester en vérifiant que les objets retournés correspondent aux valeurs attendues.
Tester les reducers : Comme les reducers sont des fonctions pures, on peut les tester en leur fournissant un état initial et une action, puis en vérifiant que le nouvel état est correct.
Tester les actions asynchrones (middleware) : Pour tester des actions asynchrones, on peut utiliser des mocks (faux appels API) et vérifier que les bonnes actions sont dispatchées dans le bon ordre.
Slice
Le state slicing permet de diviser l'état global en plusieurs "slices" (tranches), où chaque slice représente une partie indépendante de l'état de l'application (par exemple, user, posts, comments).
Cette pratique est recommandée car :
Elle permet de mieux organiser et gérer l'état complexe en découpant le state en morceaux plus petits et spécifiques.
Elle facilite la réutilisation et le découplage des reducers, car chaque slice a son propre reducer qui gère uniquement la partie de l'état qui lui est assignée.
Cela améliore la maintenabilité à long terme. Au lieu d'avoir un énorme reducer gérant tout l'état global, chaque slice de l'état peut évoluer indépendamment des autres.
Selector
Les selectors sont des fonctions qui prennent l'état global comme argument et retournent une partie spécifique de cet état. Ils sont utilisés pour isoler la logique de sélection de l'état dans une fonction dédiée, ce qui permet de centraliser et de simplifier le code d'accès aux données.
Selectors mémorisés (reselect) : Les selectors peuvent être optimisés à l'aide de la bibliothèque reselect pour éviter des recalculs inutiles. Les sélecteurs mémorisés ne recalculent une valeur que si les entrées ont changé. Cela améliore les performances en évitant les re-rendus inutiles des composants qui utilisent ces données.
Redux Vs Context
Les deux permettent de gérer l'état global, Redux :Conçu pour la gestion centralisée et complexe de l'état dans des applications de grande taille. il permet de suivre l'historique de l'état, de le déboguer facilement avec les Redux devTools Il favorise une architecture plus stricte et organisée (actions, reducers, etc.).
Context API : il est adapté pour des petites applications ou pour partager des états simples entre les composants sans passer par les props.
Plus léger et intégré directement dans React, sans besoin d'installer une bibliothèque externe. Il est moins structuré que Redux, et devient difficile à utiliser dans des applications avec des états complexes ou des besoins avancés comme les middlewares.
Dynamic reducer
Dans des applications de grande taille, il peut être nécessaire d'ajouter des reducers dynamiquement (par exemple lors du chargement de modules ou de fonctionnalités spécifiques). Cela permet de charger uniquement les reducers nécessaires lorsque certaines parties de l'application sont utilisées, réduisant ainsi la taille initiale du bundle et améliorant les performances de chargement.
Cela peut être réalisé via un mécanisme de code splitting dans Redux, où les reducers sont ajoutés dynamiquement au store.
Redux Toolkit (RTK)
Redux Toolkit (RTK) est une abstraction de Redux qui vise à simplifier l'utilisation de Redux et à réduire le boilerplate. Il est conçu pour résoudre plusieurs problèmes associés à Redux traditionnel :
Boilerplate important : Redux traditionnel nécessite beaucoup de code répétitif, comme les actions, types d'actions, et la configuration des reducers.
Configuration complexe : La mise en place d'une architecture Redux avec des middlewares comme thunk ou saga peut être fastidieuse.
RTK propose des fonctionnalités pour résoudre ces problèmes :
configureStore : Crée un store avec une configuration par défaut, intégrant déjà des middlewares comme redux-thunk.
createSlice : Combine la création des actions et du reducer dans une seule fonction, réduisant considérablement le code.
createAsyncThunk : Simplifie la gestion des actions asynchrones, comme les requêtes API, en encapsulant la logique asynchrone dans une fonction simple.
immer intégré : Permet de muter l'état de manière immuable dans les reducers sans avoir besoin d'écrire du code complexe pour gérer les copies profondes d'objets.
CombinerReducers
combineReducers est une fonction utilitaire fournie par Redux pour combiner plusieurs reducers en un seul grand reducer. Cela permet de diviser la logique de l'état en plusieurs petites fonctions spécialisées qui gèrent des parties distinctes de l'état global.
Architecture de redux
L'architecture de Redux est basée sur un modèle prévisible et centralisé de gestion d'état. Elle s'inspire du pattern Flux pour structurer et gérer l'état global des applications.
1. Principe de base de Redux Il y a trois principes fondamentaux : Unicité de la source de vérité : Toute l'état de l'application est stocké dans un unique objet JavaScript appelé le store. Cela permet de centraliser et de structurer l'état dans un seul endroit accessible.
Le store est en lecture seule : L'état ne peut être modifié que par des actions, qui sont des objets décrivant ce qui s'est passé dans l'application. Cela permet de garantir que l'état ne change jamais de manière imprévisible ou non intentionnelle.
Les changements d'état se font via des fonctions pures : Pour spécifier comment l'état change en réponse à une action, vous utilisez des fonctions appelées reducers. Ce sont des fonctions pures, ce qui signifie qu'elles ne produisent pas d'effets secondaires et ne modifient pas directement l'état existant, mais renvoient un nouvel état basé sur l'action reçue.
2. Composants Clés de l'Architecture Redux L'architecture Redux comporte quatre composants principaux :
- Le Store
- Les Actions
- Les Reducers
- Les Middlewares (optionnel)
2.1 Le Store
Le store est l'endroit où l'état global de l'application est stocké. C'est un simple objet JavaScript qui contient tout l'état, ainsi que quelques méthodes pour interagir avec lui :
- getState(): Renvoie l'état actuel du store.
- dispatch(action): Permet d'envoyer une action pour modifier l'état.
- subscribe(listener): Permet de s'abonner à des changements d'état, souvent utilisé pour mettre à jour l'UI.
2.2 Les Actions
Les actions sont des objets JavaScript décrivant ce qui s'est passé dans l'application. Elles sont les seules informations qui peuvent être envoyées au store pour déclencher un changement d'état. Une action contient deux parties principales :
- type: Un string qui décrit l'événement.
- payload (optionnel): Les données nécessaires pour effectuer le changement.
2.3 Les Reducers
Les reducers sont des fonctions pures qui prennent l'état actuel et une action, et renvoient un nouvel état. Les reducers spécifient comment l'état change en réponse à une action, mais ils ne mutent jamais directement l'état existant.
3. Le Flux de Données Unidirectionnel dans Redux
L'architecture Redux repose sur un flux de données strictement unidirectionnel. Voici comment les données circulent :
- Le composant dispatch une action : Lorsqu'un utilisateur interagit avec l'interface, un composant React déclenche une action via la méthode dispatch.
L'action passe par les middlewares (facultatif) : Une fois que l'action est dispatchée, elle passe à travers les middlewares, s'ils sont configurés. Les middlewares ont la capacité d'intercepter, modifier ou même annuler une action avant qu'elle n'atteigne le reducer. Par exemple, les middlewares comme Redux Thunk ou Redux Saga permettent de gérer des actions asynchrones ou d'ajouter des side effects.
Les reducers reçoivent l'action : L'action, une fois éventuellement modifiée par les middlewares, est transmise au reducer. Ce dernier détermine comment l'état doit être mis à jour en fonction du type d'action.
Le store met à jour l'état : Après que le reducer a renvoyé un nouvel état, le store est mis à jour avec ce nouvel état. Le store ne mutera jamais directement l'état, mais remplacera l'ancien état par le nouveau.
Les composants réactifs sont notifiés : Lorsque l'état change, tous les composants qui sont abonnés à l'état via useSelector ou connect (dans le cas de React-Redux) sont notifiés du changement d'état, ce qui déclenche le re-rendu de ces composants avec les nouvelles données.
4. Avantages Clés de l'Architecture Redux
L'architecture Redux présente plusieurs avantages clés qui rendent ce modèle utile pour gérer l'état global dans les applications, en particulier les applications React :
- Centralisation de l'état global : En ayant un seul store pour toute l'application, il devient facile de suivre l'évolution de l'état global, de le déboguer et de comprendre où se produisent les changements. Cela élimine la confusion liée à la gestion d'états locaux éparpillés dans de multiples composants.
- Prévisibilité des changements d'état : Grâce aux reducers, les changements d'état sont prévisibles et traçables. Chaque action décrit explicitement ce qui se passe dans l'application, et le changement d'état suit un schéma bien défini, ce qui facilite la gestion et le débogage.
- Débogage facilité : L'intégration avec des outils comme Redux DevTools permet de suivre précisément chaque action dispatchée et de voir comment l'état a changé en réponse. Cela permet aussi de remonter dans le temps (time-travel debugging) et d'annuler des actions si nécessaire.
- Séparation des préoccupations : Les reducers, les actions et les middlewares permettent de séparer clairement la gestion de l'état, la logique métier et les effets secondaires. Cela favorise une architecture modulaire, maintenable et évolutive.
- Gestion des effets secondaires : Les middlewares comme Redux Thunk ou Redux Saga permettent de gérer les opérations asynchrones (comme les appels API) de manière élégante et bien structurée. Cela centralise la gestion des effets secondaires et les rend plus faciles à contrôler.
mapStateToProps Vs mapDispatchToProps
Dans React-Redux, la fonction connect permet de lier les composants React au store Redux en leur fournissant l'état global et les actions comme des props. Les deux principales fonctions utilisées avec connect sont :
- mapStateToProps
- mapDispatchToProps
Ces deux fonctions servent des objectifs différents et travaillent avec connect pour relier les composants aux différentes parties de l'écosystème Redux.
mapStateToProps : Connecter le State Redux aux Props
mapStateToProps est une fonction qui permet de sélectionner une partie de l'état du store Redux et de l'injecter en tant que props dans le composant. Cela permet à votre composant de lire les données du store Redux.
Fonctionnement : Elle prend l'état global de Redux (le store) comme argument. Elle renvoie un objet où les clés sont des props du composant et les valeurs sont les parties de l'état global que vous souhaitez passer au composant.
mapDispatchToProps : Connecter les Actions Redux aux Props
mapDispatchToProps est une fonction qui permet de lier les action creators à dispatch et de les injecter en tant que props dans le composant. Cela permet à votre composant de déclencher des actions pour modifier l'état du store.
Fonctionnement :
Elle prend dispatch (la fonction pour dispatcher des actions dans Redux) comme argument. Elle renvoie un objet où les clés sont des props, et les valeurs sont des fonctions qui dispatchent les actions.
Avec RTK, on peut utiliser avec les hooks (useSelector et useDispatch)
Avec les versions récentes de React-Redux, il est possible de ne plus utiliser connect et de se tourner vers les hooks :
- useSelector pour remplacer mapStateToProps.
- useDispatch pour remplacer mapDispatchToProps.
Thunk Vs Saga
Redux-thunk et redux-saga sont deux middlewares de Redux qui permettent de gérer des opérations asynchrones dans une application. Ils ont des approches différentes pour résoudre le même problème: comment interagir avec l'API et effectuer des effets secondaires en gardant les principes de Redux.
- Complexité: Redux-Saga est plus complexe à comprendre, surtout pour les développeurs moins familiers avec les fonctions générateurs de JavaScript, tandis que Redux-Thunk utilise des fonctions qui retournent des fonctions, ce qui est généralement plus simple.
- Testabilité: Redux-Saga est souvent loué pour la facilité de test de ses sagas grâce aux effets déclaratifs, contrairement aux thunks qui nécessitent de mocker les dispatch et getState.
Optimisation Redux avec la normalisation de l'état
La normalisation de l'état dans Redux est une pratique qui permet d'optimiser la gestion de grandes quantités de données dans le store en évitant les duplications et en facilitant l'accès aux données. Cette technique s'inspire de la normalisation des bases de données relationnelles, où les données redondantes sont séparées en tables distinctes avec des relations définies via des identifiants (IDs).
Pourquoi normaliser l'état dans Redux ?
Lorsque vous travaillez avec des données imbriquées ou relationnelles, comme des utilisateurs, des articles, des commentaires, etc., les données peuvent rapidement devenir difficiles à gérer et mettre à jour si elles sont dupliquées dans plusieurs parties du store. Cela peut entraîner :
Des mises à jour incohérentes (mettre à jour une entité à plusieurs endroits).
Une gestion d'état inefficace, car chaque entité imbriquée pourrait entraîner des ré-rendus inutiles.
Des difficultés d'accès aux données, car les structures imbriquées compliquent la sélection des données.
La normalisation consiste à restructurer l'état pour stocker chaque entité comme une liste d'entrées, avec un identifiant unique (ID) pour chaque élément, et gérer les relations par référence d'ID plutôt que d'inclure les objets directement imbriqués.
les auteurs et les utilisateurs sont dupliqués dans chaque article et commentaire, ce qui rend l'état redondant et difficile à mettre à jour si, par exemple, le nom de l'auteur change.
Normalisation de cet état
Dans un état normalisé, nous allons :
Séparer les entités posts, authors, et comments.
Utiliser les identifiants (ID) pour faire référence aux relations entre les entités.
Dans cet état normalisé :
Chaque entité (post, author, comment, user) est stockée séparément, avec ses propres identifiants.
Les relations entre entités (par exemple, un post fait référence à un auteur via authorId, et à des commentaires via une liste d'IDs) sont gérées par des références d'ID plutôt que d'inclure les objets eux-mêmes.
Nous avons deux objets principaux pour chaque entité : byId (pour stocker les éléments individuels par leur ID) et allIds (pour lister tous les IDs dans l'ordre).
Avantages de la normalisation
Élimination des duplications : Les auteurs et les utilisateurs ne sont plus dupliqués dans chaque article ou commentaire. Si le nom d'un utilisateur change, il suffit de le mettre à jour dans un seul endroit.
Mises à jour simplifiées : Mettre à jour un élément devient plus facile. Si vous devez mettre à jour un auteur ou un commentaire, vous ne modifiez qu'un seul objet dans l'état.
Performances améliorées : En supprimant les objets imbriqués, vous réduisez les chances de déclencher des re-rendus inutiles dans les composants React qui dépendent de ces données.
Accès plus facile : Les données sont organisées d'une manière qui facilite leur recherche et leur gestion.
Qu'est-ce qu'un middleware dans Redux et comment fonctionne-t-il ?
Réponse :
Un middleware dans Redux est une fonction qui intercepte les actions dispatchées avant qu'elles n'atteignent les reducers. Il permet d'ajouter des fonctionnalités supplémentaires comme la gestion des effets asynchrones (par exemple, les appels API), la journalisation, ou la modification des actions.
Chaque middleware reçoit trois paramètres :
store : qui donne accès à getState et dispatch.
next : qui est la prochaine fonction à appeler (soit le prochain middleware, soit le reducer si c'est le dernier middleware).
action : l'action qui a été dispatchée.
2. Comment fonctionne le cycle de vie complet d'une action dans Redux ?
Réponse :
Le cycle de vie d'une action dans Redux peut être décomposé en plusieurs étapes :
Dispatch d'une action : Une action est dispatchée depuis un composant (généralement via dispatch(action)).
Middlewares : L'action traverse la chaîne des middlewares si elle est configurée. Les middlewares peuvent intercepter, modifier, ou même empêcher l'action d'atteindre les reducers.
Reducers : Une fois que l'action traverse tous les middlewares, elle arrive dans les reducers. Chaque reducer reçoit l'état actuel et l'action, puis décide s'il doit modifier l'état.
État mis à jour : Si un reducer retourne un nouvel état, cet état est mis à jour dans le store, et le cycle est complet.
Notification des composants : Après la mise à jour de l'état, Redux notifie les composants abonnés (connect ou useSelector) pour qu'ils se ré-rendent en fonction du nouvel état.
---
Avis sur Redux
Redux est une très bonne solution pour gérer un état global complexe, surtout quand il y a beaucoup d'interactions entre différentes parties de l'application.
Mais aujourd'hui, avec l'évolution de l'écosystème React, je préfère l'utiliser uniquement quand c'est justifié.
Par exemple, pour la gestion des données serveur, j'utilise plutôt React Query, et pour des états plus simples, des solutions comme Zustand ou même le state React suffisent largement.
Redux reste pertinent, mais il faut éviter de l'utiliser par défaut.
Les limites aujourd'hui
- boilerplate (même avec Redux Toolkit)
- parfois overkill
- complexité inutile pour petits besoins
- pas optimal pour data fetching
Limite de redux
Disadvantages of Redux
Alternative Solutions:
Redux Vs Context
Redux:
- Promotes a strict and structured architecture (with actions, reducers, middleware, etc.).
Context API
Dynamic reducer
This approach:
- Helps reduce initial bundle size
- Improves loading performance
Architecture de redux
Core Principles of Redux
Redux is built on three main principles: