L'architecture Flux résonne dans tous les gestionnaires d'état React modernes. Les bibliothèques atomiques répondent mieux que Flux à la vision originale de Flux en offrant une meilleure évolutivité, autonomie, division du code, gestion du cache, organisation du code et primitives pour le partage d'état.
Recoil a introduit le modèle atomique dans le monde React . Ses nouveaux pouvoirs se sont fait au prix d'une courbe d'apprentissage abrupte et de ressources d'apprentissage rares.
et ont ensuite simplifié divers aspects de ce nouveau modèle, offrant de nombreuses nouvelles fonctionnalités et repoussant les limites de ce nouveau paradigme étonnant.
D'autres articles porteront sur les différences entre ces outils. Cet article se concentrera sur une grande fonctionnalité que les 3 ont en commun :
Ils ont corrigé Flux.
Table des matières
Flux
Arbres de dépendance
Le modèle singleton
Retour aux sources
La loi de Déméter
Les héros
Zedux
Le modèle atomique
Conclusion
Flux
Si vous ne connaissez pas Flux, voici un aperçu rapide :
Outre Redux , toutes les bibliothèques basées sur Flux ont essentiellement suivi ce modèle : une application a plusieurs magasins. Il n'y a qu'un seul répartiteur dont le travail consiste à fournir des actions à tous les magasins dans le bon ordre. Cet "ordre approprié" signifie trier dynamiquement les dépendances entre les magasins.
Prenons l'exemple d'une configuration d'application de commerce électronique :
Lorsque l'utilisateur déplace, par exemple, une banane vers son panier, le PromosStore doit attendre que l'état de CartStore soit mis à jour avant d'envoyer une demande pour voir s'il y a un coupon de banane disponible.
Ou peut-être que les bananes ne peuvent pas être expédiées dans la zone de l'utilisateur. Le CartStore doit vérifier le UserStore avant la mise à jour. Ou peut-être que les coupons ne peuvent être utilisés qu'une fois par semaine. Le PromosStore doit vérifier le UserStore avant d'envoyer la demande de coupon.
Flux n'aime pas ces dépendances. À partir des :
Les objets d'une application Flux sont fortement découplés et adhèrent très fortement à la , le principe selon lequel chaque objet d'un système doit en savoir le moins possible sur les autres objets du système.
La théorie derrière cela est solide. 100 %. Soo ... pourquoi cette saveur multi-magasins de Flux est-elle morte?
Arbres de dépendance
Il s'avère que les dépendances entre les conteneurs d'états isolés sont inévitables. En fait, pour garder le code modulaire et DRY, vous devriez utiliser fréquemment d'autres magasins.
Dans Flux, ces dépendances sont créées à la volée :
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
Cet exemple montre comment les dépendances ne sont pas directement déclarées entre les magasins - elles sont plutôt reconstituées action par action. Ces dépendances informelles nécessitent de creuser dans le code d'implémentation pour les trouver.
C'est un exemple très simple ! Mais vous pouvez déjà voir à quel point Flux se sent désordonné. Les effets secondaires, les opérations de sélection et les mises à jour d'état sont tous bricolés. Cette colocation peut en fait être plutôt agréable. Mais mélangez quelques dépendances informelles, triplez la recette et servez-la sur un passe-partout et vous verrez Flux se décomposer rapidement.
D'autres implémentations de Flux comme Flummox et Reflux ont amélioré l'expérience passe-partout et de débogage. Bien que très utilisable, la gestion des dépendances était le seul problème persistant qui tourmentait toutes les implémentations de Flux. Utiliser un autre magasin était moche. Les arbres de dépendance profondément imbriqués étaient difficiles à suivre.
Cette application de commerce électronique pourrait un jour avoir des magasins pour OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded, etc. Une grande application pourrait facilement avoir des centaines de magasins. Comment maintenez-vous les dépendances à jour dans chaque magasin ? Comment suivre les effets secondaires ? Qu'en est-il de la pureté ? Qu'en est-il du débogage ? La banane est-elle vraiment une baie ?
En ce qui concerne les principes de programmation introduits par Flux, le flux de données unidirectionnel était un gagnant, mais, pour l'instant, la loi de Demeter ne l'était pas.
Le modèle singleton
Nous savons tous comment Redux est arrivé en rugissant pour sauver la situation. Il a abandonné le concept de magasins multiples au profit d'un modèle singleton. Désormais, tout peut accéder à tout le reste sans aucune "dépendance".
Les réducteurs sont purs, donc toute logique traitant de plusieurs tranches d'état doit sortir du magasin. La communauté a établi des normes pour la gestion des effets secondaires et de l'état dérivé. Les magasins Redux sont magnifiquement débogables. Le seul défaut majeur de Flux que Redux n'a pas réussi à corriger à l'origine était son passe-partout.
a ensuite simplifié le tristement célèbre passe-partout de Redux. Ensuite, Zustand a supprimé quelques peluches au prix d'une certaine puissance de débogage. Tous ces outils sont devenus extrêmement populaires dans le monde React.
Avec l'état modulaire, les arbres de dépendance deviennent si naturellement complexes que la meilleure solution à laquelle nous pouvions penser était : "Ne le faites pas, je suppose."
Et ça a marché ! Cette nouvelle approche singleton fonctionne encore assez bien pour la plupart des applications. Les principes de Flux étaient si solides que la simple suppression du cauchemar de la dépendance l'a résolu.
Ou l'a-t-il fait ?
Retour aux sources
Le succès de l'approche singleton soulève la question suivante : vers quoi Flux voulait-il en venir ? Pourquoi avons-nous jamais voulu plusieurs magasins?
Permettez-moi de faire la lumière là-dessus.
Raison #1 : Autonomie
Avec plusieurs magasins, les pièces d'État sont réparties dans leurs propres conteneurs autonomes et modulaires. Ces magasins peuvent être testés isolément. Ils peuvent également être partagés facilement entre les applications et les packages.
Raison #2 : Fractionnement de code
Ces magasins autonomes peuvent être divisés en morceaux de code distincts. Dans un navigateur, ils peuvent être chargés paresseusement et branchés à la volée.
Les réducteurs de Redux sont également assez faciles à diviser en code. Grâce à replaceReducer , la seule étape supplémentaire consiste à créer le nouveau réducteur combiné. Cependant, d'autres étapes peuvent être nécessaires lorsque des effets secondaires et des intergiciels sont impliqués.
Raison #3 : Primitives Standardisées
Avec le modèle singleton, il est difficile de savoir comment intégrer l'état interne d'un module externe au sien. La communauté Redux a introduit le modèle Ducks pour tenter de résoudre ce problème. Et ça marche, au prix d'un petit passe-partout.
Avec plusieurs magasins, un module externe peut simplement exposer un magasin. Par exemple, une bibliothèque de formulaires peut exporter un FormStore. L'avantage est que la norme est "officielle", ce qui signifie que les gens sont moins susceptibles de créer leurs propres méthodologies. Cela conduit à une communauté et à un écosystème de packages plus robustes et unifiés.
Raison #4 : Évolutivité
Le modèle singleton est étonnamment performant. Redux l'a prouvé. Cependant, son modèle de sélection a surtout une limite supérieure dure. J'ai écrit quelques réflexions à ce sujet dans . Un grand arbre de sélection coûteux peut vraiment commencer à traîner, même en prenant un contrôle maximal sur la mise en cache.
D'autre part, avec plusieurs magasins, la plupart des mises à jour d'état sont isolées dans une petite partie de l'arborescence d'état. Ils ne touchent à rien d'autre dans le système. Ceci est évolutif bien au-delà de l'approche singleton - en fait, avec plusieurs magasins, il est très difficile d'atteindre les limitations du processeur avant d'atteindre les limitations de la mémoire sur la machine de l'utilisateur.
Raison #5 : Destruction
La destruction de l'état n'est pas trop difficile dans Redux. Tout comme dans l'exemple de fractionnement de code, il suffit de quelques étapes supplémentaires pour supprimer une partie de la hiérarchie des réducteurs. Mais c'est encore plus simple avec plusieurs magasins - en théorie, vous pouvez simplement détacher le magasin du répartiteur et lui permettre d'être ramassé.
Raison #6 : Colocation
C'est le gros problème que Redux, Zustand et le modèle singleton en général ne gèrent pas bien. Les effets secondaires sont séparés de l'état avec lequel ils interagissent. La logique de sélection est séparée de tout. Alors que Flux multi-magasins était peut-être trop colocalisé, Redux est allé à l'extrême opposé.
Avec plusieurs magasins autonomes, ces choses vont naturellement de pair. Vraiment, Flux ne manquait que de quelques normes pour éviter que tout ne devienne un méli-mélo de charabia (désolé).
Résumé des motifs
Maintenant, si vous connaissez la bibliothèque OG Flux, vous savez que ce n'était pas génial du tout. Le répartiteur adopte toujours une approche globale - répartissant chaque action dans chaque magasin. Le tout avec des dépendances informelles/implicites a également rendu le fractionnement et la destruction du code moins que parfaits.
Pourtant, Flux avait beaucoup de fonctionnalités intéressantes. De plus, l'approche multi-magasins offre un potentiel pour encore plus de fonctionnalités telles que l'inversion de contrôle et la gestion de l'état fractal (c'est-à-dire local).
Flux aurait pu évoluer en un gestionnaire d'état vraiment puissant si quelqu'un n'avait pas nommé sa déesse Déméter. Je suis sérieux! ... D'accord, je ne le suis pas. Mais maintenant que vous en parlez, peut-être que la loi de Déméter mérite d'être examinée de plus près :
La loi de Déméter
Quelle est exactement cette soi-disant "loi"? De :
Chaque unité ne doit avoir qu'une connaissance limitée des autres unités : uniquement les unités "étroitement" liées à l'unité actuelle.
Chaque unité ne doit parler qu'à ses amis ; ne parlez pas aux étrangers.
Cette loi a été conçue avec la programmation orientée objet à l'esprit, mais elle peut être appliquée dans de nombreux domaines, y compris la gestion de l'état de React.
L'idée de base est d'empêcher un magasin de :
Se couple étroitement aux détails de mise en œuvre d'un autre magasin.
Utiliser des magasins qu'il n'a pas besoin de connaître .
Utiliser n'importe quel autre magasin sans déclarer explicitement une dépendance à ce magasin.
En termes de banane, une banane ne devrait pas éplucher une autre banane et ne devrait pas parler à une banane dans un autre arbre. Cependant, il peut parler à l'autre arbre si les deux arbres installent d'abord une ligne téléphonique banane.
Cela encourage la séparation des préoccupations et aide votre code à rester modulaire, SEC et SOLIDE. Théorie solide ! Alors, qu'est-ce qui manquait à Flux ?
Eh bien, les dépendances inter-magasins font naturellement partie d'un bon système modulaire. Si un magasin doit ajouter une autre dépendance, il doit le faire et le faire aussi explicitement que possible . Voici à nouveau une partie de ce code Flux :
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore a plusieurs dépendances déclarées de différentes manières - il attend et lit à partir de CartStoreet il lit à partir de UserStore . La seule façon de découvrir ces dépendances est de rechercher des magasins dans l'implémentation de PromosStore.
Les outils de développement ne peuvent pas non plus aider à rendre ces dépendances plus détectables. En d'autres termes, les dépendances sont trop implicites.
Bien qu'il s'agisse d'un exemple très simple et artificiel, il illustre comment Flux a mal interprété la loi de Déméter. Bien que je sois sûr que cela soit principalement né d'un désir de garder les implémentations de Flux petites (la vraie gestion des dépendances est une tâche complexe !), c'est là que Flux a échoué.
Contrairement aux héros de cette histoire :
Les héros
En 2020, est entré en trébuchant sur la scène. Bien qu'un peu maladroit au début, il nous a appris un nouveau modèle qui a ravivé l'approche multi-magasins de Flux.
Flux de données unidirectionnel déplacé du magasin lui-même vers le graphique de dépendance. Les magasins s'appelaient désormais des atomes. Les atomes étaient correctement autonomes et fractionnables en code. Ils avaient de nouveaux pouvoirs comme le soutien du suspense et l'hydratation. Et surtout, les atomes déclarent formellement leurs dépendances.
Le modèle atomique est né.
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil a lutté avec une base de code gonflée, des fuites de mémoire, de mauvaises performances, un développement lent et des fonctionnalités instables - notamment des effets secondaires. Cela éliminerait lentement certaines d'entre elles, mais entre-temps, d'autres bibliothèques ont repris les idées de Recoil et les ont suivies.
a fait irruption sur la scène et a rapidement gagné un public.
// a Jotai atom const greetingAtom = atom('Hello, World!')
En plus d'être une infime fraction de la taille de Recoil, Jotai offrait de meilleures performances, des API plus élégantes et aucune fuite de mémoire grâce à son approche basée sur WeakMap.
Cependant, cela s'est fait au prix d'une certaine puissance - l'approche WeakMap rend le contrôle du cache difficile et le partage de l'état entre plusieurs fenêtres ou d'autres domaines presque impossible. Et le manque de clés de chaîne, bien qu'élégant, fait du débogage un cauchemar. La plupart des applications devraient les rajouter, ce qui ternirait considérablement l'élégance de Jotai.
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
Quelques mentions honorables sont et . Ces bibliothèques ont exploré davantage la théorie derrière le modèle atomique et essaient de pousser sa taille et sa vitesse à la limite.
Le modèle atomique est rapide et évolue très bien. Mais jusqu'à très récemment, il y avait quelques problèmes qu'aucune bibliothèque atomique n'avait très bien résolus :
La courbe d'apprentissage. Les atomes sont différents . Comment rendons-nous ces concepts accessibles aux développeurs de React ?
Dev X et débogage. Comment rendons-nous les atomes détectables ? Comment suivez-vous les mises à jour ou appliquez-vous les bonnes pratiques ?
Migration incrémentielle pour les bases de code existantes. Comment accéder aux boutiques externes ? Comment gardez-vous la logique existante intacte ? Comment éviter une réécriture complète ?
Plugins. Comment rendre le modèle atomique extensible ? Peut- il gérer toutes les situations possibles ?
Injection de dépendance. Les atomes définissent naturellement les dépendances, mais peuvent-ils être échangés pendant les tests ou dans différents environnements ?
La loi de Déméter. Comment masquer les détails de mise en œuvre et empêcher les mises à jour dispersées ?
C'est là que j'interviens. Vous voyez, je suis le principal créateur d'une autre bibliothèque atomique :
Zedux
Zedux est enfin entré en scène il y a quelques semaines. Développé par une société Fintech à New York - la société pour laquelle je travaille - Zedux n'a pas seulement été conçu pour être rapide et évolutif, mais également pour offrir une expérience de développement et de débogage élégante.
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
Je n'entrerai pas dans les détails des fonctionnalités de Zedux ici - comme je l'ai dit, cet article ne se concentrera pas sur les différences entre ces bibliothèques atomiques.
Qu'il suffise de dire que Zedux répond à toutes les préoccupations ci-dessus. Par exemple, c'est la première bibliothèque atomique à proposer une véritable inversion de contrôle et la première à nous ramener à la loi de Déméter en proposant pour masquer les détails d'implémentation.
Les dernières idéologies de Flux ont finalement été relancées - non seulement relancées, mais améliorées ! - grâce au modèle atomique.
Alors, quel est exactement le modèle atomique ?
Le modèle atomique
Ces bibliothèques atomiques présentent de nombreuses différences - elles ont même des définitions différentes de ce que signifie "atomique". Le consensus général est que les atomes sont de petits conteneurs d'états isolés et autonomes mis à jour de manière réactive via un graphe acyclique dirigé.
Je sais, je sais, ça semble complexe, mais attendez juste que je l'explique avec des bananes.
Je plaisante! C'est en fait très simple :
Les mises à jour ricochent sur le graphique. C'est ça!
Le fait est que, quelle que soit l'implémentation ou la sémantique, toutes ces bibliothèques atomiques ont ravivé le concept de magasins multiples et les ont rendus non seulement utilisables, mais aussi un vrai plaisir de travailler avec.
Les 6 raisons que j'ai données pour vouloir plusieurs magasins sont exactement les raisons pour lesquelles le modèle atomique est si puissant :
Autonomie - Les atomes peuvent être testés et utilisés complètement isolés.
Fractionnement de code - Importez un atome et utilisez-le ! Aucune considération supplémentaire requise.
Primitives standardisées - Tout peut exposer un atome pour une intégration automatique.
Évolutivité - Les mises à jour n'affectent qu'une petite partie de l'arborescence d'état.
Destruction - Arrêtez simplement d'utiliser un atome et tout son état est récupéré.
Colocation - Les atomes définissent naturellement leur propre état, leurs effets secondaires et leur logique de mise à jour.
Les API simples et l'évolutivité font à elles seules des bibliothèques atomiques un excellent choix pour chaque application React. Plus de puissance et moins passe-partout que Redux ? Est-ce un rêve ?
Conclusion
Quel voyage ! Le monde de la gestion d'état React ne cesse de surprendre, et je suis tellement content d'avoir fait du stop.
Nous ne faisons que commencer. Il y a beaucoup de place pour l'innovation avec les atomes. Après avoir passé des années à créer et à utiliser Zedux, j'ai vu à quel point le modèle atomique peut être puissant. En fait, sa puissance est son talon d'Achille :
Lorsque les développeurs explorent les atomes, ils creusent souvent si profondément dans les possibilités qu'ils reviennent en disant : "Regardez ce pouvoir complexe fou", plutôt que "Regardez à quel point les atomes résolvent ce problème simplement et avec élégance". Je suis ici pour changer ça.
Le modèle atomique et la théorie qui le sous-tend n'ont pas été enseignés d'une manière accessible à la plupart des développeurs de React. D'une certaine manière, l'expérience des atomes dans le monde React a jusqu'à présent été à l'opposé de Flux :
Cet article est le deuxième d'une série de ressources d'apprentissage que je produis pour aider les développeurs de React à comprendre comment fonctionnent les bibliothèques atomiques et pourquoi vous voudrez peut-être en utiliser une. Consultez le premier article - Évolutivité : le niveau perdu de la gestion de l'état de réaction .
Cela a pris 10 ans, mais la solide théorie CS introduite par Flux a enfin un impact considérable sur les applications React grâce au modèle atomique. Et il continuera à le faire pour les années à venir.