Flux de données simple dans les applications React utilisant Flux et Backbone : un didacticiel avec des exemples

Publié: 2022-03-11

React.js est une bibliothèque fantastique. Parfois, cela semble être la meilleure chose depuis Python tranché. Cependant, React n'est qu'une partie d'une pile d'applications frontales. Il n'a pas grand-chose à offrir en matière de gestion des données et de l'état.

Facebook, les créateurs de React, y ont proposé des conseils sous la forme de Flux. Flux est une «architecture d'application» (pas un cadre) construite autour d'un flux de données unidirectionnel utilisant React Views, un répartiteur d'action et des magasins. Le modèle Flux résout certains problèmes majeurs en incarnant des principes importants de contrôle des événements, ce qui rend les applications React beaucoup plus faciles à raisonner, à développer et à maintenir.

Ici, je vais présenter des exemples de flux de contrôle de base de Flux, discuter de ce qui manque pour les magasins et comment utiliser les modèles et les collections de backbone pour combler le vide d'une manière « conforme à Flux ».

(Remarque : j'utilise CoffeeScript dans mes exemples pour plus de commodité et de brièveté. Les développeurs non-CoffeeScript devraient pouvoir suivre et traiter les exemples comme du pseudocode.)

Présentation du Flux de Facebook

Backbone est une excellente petite bibliothèque bien contrôlée qui comprend des vues, des modèles, des collections et des itinéraires. Il s'agit d'une bibliothèque standard de facto pour les applications frontales structurées, et elle est associée aux applications React depuis l'introduction de cette dernière en 2013. La plupart des exemples de React en dehors de Facebook.com ont jusqu'à présent inclus des mentions d'utilisation de Backbone en tandem.

Malheureusement, s'appuyer uniquement sur Backbone pour gérer l'intégralité du flux d'application en dehors des vues de React présente des complications malheureuses. Lorsque j'ai commencé à travailler sur le code de l'application React-Backbone, les « chaînes d'événements complexes » dont j'avais entendu parler n'ont pas tardé à faire leur apparition. L'envoi d'événements de l'interface utilisateur aux modèles, puis d'un modèle à un autre, puis inversement, rend difficile le suivi de qui a changé qui, dans quel ordre et pourquoi.

Ce didacticiel Flux montrera comment le modèle Flux gère ces problèmes avec une facilité et une simplicité impressionnantes.

Un aperçu

Le slogan de Flux est « flux de données unidirectionnel ». Voici un diagramme pratique de la documentation Flux montrant à quoi ressemble ce flux :

Facebook Flux utilise un modèle de "flux de données unidirectionnel" qui varie un peu lorsqu'il est associé à React et Backbone.

Le bit important est que les choses proviennent de React --> Dispatcher --> Stores --> React .

Regardons ce que sont chacun des composants principaux et comment ils se connectent :

Les docs offrent également cette mise en garde importante :

Flux est plus un modèle qu'un framework et n'a pas de dépendances matérielles. Cependant, nous utilisons souvent EventEmitter comme base pour les Stores et React pour nos Views. Le seul élément de Flux qui n'est pas facilement disponible ailleurs est le Dispatcher. Ce module est disponible ici pour compléter votre boîte à outils Flux.

Flux a donc trois composants :

  1. Vues ( React = require('react') )
  2. Dispatcher ( Dispatcher = require('flux').Dispatcher )
  3. Magasins ( EventEmitter = require('events').EventEmitter )
    • (ou, comme nous le verrons bientôt, Backbone = require('backbone') )

Les vues

Je ne décrirai pas React ici, car tant de choses ont été écrites à ce sujet, si ce n'est pour dire que je le préfère largement à Angular. Je ne me sens presque jamais confus lors de l'écriture de code React, contrairement à Angular, mais bien sûr, les opinions varient.

Le répartiteur

Le Flux Dispatcher est un lieu unique où sont gérés tous les événements qui modifient vos Boutiques. Pour l'utiliser, chaque magasin register un seul rappel pour gérer tous les événements. Ensuite, chaque fois que vous souhaitez modifier un Store, vous dispatch un événement.

Comme React, le Dispatcher me semble être une bonne idée, bien mise en œuvre. Par exemple, une application qui permet à l'utilisateur d'ajouter des éléments à une liste de tâches peut inclure les éléments suivants :

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes!. module.exports = TodoDispatcher
 # in TodoStore.coffee TodoDispatcher = require("./TodoDispatcher") TodoStore = {items: []} TodoStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" TodoStore.items.push payload.item when "delete-last-item" TodoStore.items.pop() TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback) module.exports = TodoStore
 # in ItemAddComponent.coffee TodoDispatcher = require("./TodoDispatcher") ItemAddComponent = React.createClass handleAddItem: -> # note: you're NOT just pushing directly to the store! # (the restriction of moving through the dispatcher # makes everything much more modular and maintainable) TodoDispatcher.dispatch actionType: "add-item" item: "hello world" render: -> React.DOM.button { onClick: @handleAddItem }, "Add an Item!"

Cela permet de répondre très facilement à deux questions :

  1. Q : Quels sont tous les événements qui modifient MyStore ?
    • R : Vérifiez simplement les cas dans l'instruction switch dans MyStore.dispatchCallback .
  2. Q : Quelles sont toutes les sources possibles de cet événement ?
    • R : Recherchez simplement cet actionType .

C'est beaucoup plus facile que, par exemple, de rechercher MyModel.set et MyModel.save et MyCollection.add , etc., où la recherche des réponses à ces questions de base devient très difficile, très rapide.

Le répartiteur vous permet également d'exécuter des rappels séquentiellement de manière simple et synchrone, à l'aide waitFor . Par exemple:

 # in MessageStore.coffee MyDispatcher = require("./MyDispatcher") TodoStore = require("./TodoStore") MessageStore = {items: []} MessageStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" # synchronous event flow! MyDispatcher.waitFor [TodoStore.dispatchToken] MessageStore.items.push "You added an item! It was: " + payload.item module.exports = MessageStore

En pratique, j'ai été choqué de voir à quel point mon code était plus propre lorsque j'utilisais le Dispatcher pour modifier mes Stores, même sans utiliser waitFor .

Les magasins

Ainsi, les données circulent dans les magasins via le répartiteur. J'ai compris. Mais comment les données circulent-elles des Stores vers les Views (c'est-à-dire React) ? Comme indiqué dans la documentation Flux :

[La] vue écoute les événements diffusés par les magasins dont elle dépend.

D'accord! Super. Tout comme nous avons enregistré des rappels avec nos magasins, nous enregistrons des rappels avec nos vues (qui sont des composants React). Nous disons à React de re- render chaque fois qu'un changement se produit dans le Store qui a été transmis via ses props .

Par exemple:

 # in TodoListComponent.coffee React = require("react") TodoListComponent = React.createClass componentDidMount: -> @props.TodoStore.addEventListener "change", => @forceUpdate() , @ componentWillUnmount: -> # remove the callback render: -> # show the items in a list. React.DOM.ul {}, @props.TodoStore.items.map (item) -> React.DOM.li {}, item

Impressionnant!

Alors, comment émettons-nous cet événement "change" ? Eh bien, Flux recommande d'utiliser EventEmitter . A partir d'un exemple officiel :

 var MessageStore = merge(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, /** * @param {function} callback */ addChangeListener: function(callback) { this.on(CHANGE_EVENT, callback); }, get: function(id) { return _messages[id]; }, getAll: function() { return _messages; }, // etc...

Brut! Je dois écrire tout ça moi-même, à chaque fois que je veux un simple Store ? Que dois-je utiliser chaque fois que j'ai une information à afficher ? Il doit y avoir une meilleure façon!

La pièce manquante

Les modèles et les collections de Backbone ont déjà tout ce que les magasins basés sur EventEmitter de Flux semblent faire.

En vous disant d'utiliser Raw EventEmitter, Flux vous recommande de recréer peut-être 50 à 75 % des modèles et collections de Backbone à chaque fois que vous créez un magasin. Utiliser EventEmitter pour vos magasins revient à utiliser Node.js nu pour votre serveur lorsque des microframeworks bien construits comme Express.js ou équivalent existent déjà pour prendre en charge toutes les bases et le passe-partout.

Tout comme Express.js est construit sur Node.js, les modèles et les collections de Backbone sont construits sur EventEmitter. Et il a tout ce dont vous avez presque toujours besoin : Backbone émet des événements change et a des méthodes de requête, des getters et des setters et tout. De plus, Jeremy Ashkenas de Backbone et son armée de 230 contributeurs ont fait un bien meilleur travail sur toutes ces choses que je suis susceptible de pouvoir faire.

À titre d'exemple pour ce didacticiel Backbone, j'ai converti l'exemple MessageStore ci-dessus en une version Backbone.

C'est objectivement moins de code (pas besoin de dupliquer le travail) et c'est subjectivement plus clair et concis (par exemple, this.add(message) au lieu de _messages[message.id] = message ).

Alors utilisons Backbone for Stores !

Le modèle FluxBone : Flux Stores par Backbone

Ce tutoriel est la base d'une approche que j'ai fièrement surnommée FluxBone , l'architecture Flux utilisant Backbone for Stores. Voici le schéma de base d'une architecture FluxBone :

  1. Les magasins sont des modèles ou des collections de backbone instanciés, qui ont enregistré un rappel auprès du répartiteur. En règle générale, cela signifie qu'ils sont des singletons.
  2. Les composants de vue ne modifient jamais directement les magasins (par exemple, pas de .set() ). Au lieu de cela, les composants envoient des actions au répartiteur.
  3. Affichez les requêtes des composants Stores et liez-les à leurs événements pour déclencher des mises à jour.

Ce didacticiel Backbone est conçu pour examiner la manière dont Backbone et Flux fonctionnent ensemble dans les applications React.

Utilisons les exemples Backbone et Flux pour examiner chaque élément tour à tour :

1. Les magasins sont des modèles ou des collections de backbone instanciés, qui ont enregistré un rappel auprès du répartiteur.

 # in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes! module.exports = TodoDispatcher
 # in stores/TodoStore.coffee Backbone = require("backbone") TodoDispatcher = require("../dispatcher") TodoItem = Backbone.Model.extend({}) TodoCollection = Backbone.Collection.extend model: TodoItem url: "/todo" # we register a callback with the Dispatcher on init. initialize: -> @dispatchToken = TodoDispatcher.register(@dispatchCallback) dispatchCallback: (payload) => switch payload.actionType # remove the Model instance from the Store. when "todo-delete" @remove payload.todo when "todo-add" @add payload.todo when "todo-update" # do stuff... @add payload.todo, merge: true # ... etc # the Store is an instantiated Collection; a singleton. TodoStore = new TodoCollection() module.exports = TodoStore

2. Les composants ne modifient jamais directement les Stores (par exemple, pas de .set() ). Au lieu de cela, les composants envoient des actions au répartiteur.

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the Dispatcher TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... (see below) ... module.exports = TodoListComponent

3. Les composants interrogent les magasins et se lient à leurs événements pour déclencher des mises à jour.

 # components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... componentDidMount: -> # the Component binds to the Store's events @props.TodoStore.on "add remove reset", => @forceUpdate() , @ componentWillUnmount: -> # turn off all events and callbacks that have this context @props.TodoStore.off null, null, this render: -> React.DOM.ul {}, @props.TodoStore.items.map (todoItem) -> # TODO: TodoItemComponent, which would bind to # `this.props.todoItem.on('change')` TodoItemComponent { todoItem: todoItem } module.exports = TodoListComponent

J'ai appliqué cette approche Flux et Backbone à mes propres projets, et une fois que j'ai repensé mon application React pour utiliser ce modèle, presque tous les éléments laids ont disparu. C'était un peu miraculeux : un par un, les morceaux de code qui me faisaient grincer des dents à la recherche d'un meilleur moyen ont été remplacés par un flux sensé. Et la fluidité avec laquelle Backbone semble s'intégrer dans ce schéma est remarquable : je n'ai pas l'impression de me battre contre Backbone, Flux ou React pour les emboîter dans une seule application.

Exemple Mixin

Écrire le this.on(...) et this.off(...) chaque fois que vous ajoutez un FluxBone Store à un composant peut devenir un peu vieux.

Voici un exemple React Mixin qui, bien qu'extrêmement naïf, rendrait certainement l'itération rapide encore plus facile :

 # in FluxBoneMixin.coffee module.exports = (propName) -> componentDidMount: -> @props[propName].on "all", => @forceUpdate() , @ componentWillUnmount: -> @props[propName].off "all", => @forceUpdate() , @
 # in HelloComponent.coffee React = require("react") UserStore = require("./stores/UserStore") TodoStore = require("./stores/TodoStore") FluxBoneMixin = require("./FluxBoneMixin") MyComponent = React.createClass mixins: [ FluxBoneMixin("UserStore"), FluxBoneMixin("TodoStore"), ] render: -> React.DOM.div {}, "Hello, #{ @props.UserStore.get('name') }, you have #{ @props.TodoStore.length } things to do." React.renderComponent( MyComponent { UserStore: UserStore TodoStore: TodoStore } , document.body.querySelector(".main") )

Synchronisation avec une API Web

Dans le diagramme Flux d'origine, vous interagissez avec l'API Web via ActionCreators uniquement, qui nécessitent une réponse du serveur avant d'envoyer des actions au Dispatcher. Cela ne m'a jamais convenu; Le Store ne devrait-il pas être le premier informé des changements, avant le serveur ?

Je choisis d'inverser cette partie du diagramme : les Stores interagissent directement avec une API RESTful CRUD via Backbone's sync() . C'est merveilleusement pratique, du moins si vous travaillez avec une véritable API RESTful CRUD.

L'intégrité des données est maintenue sans problème. Lorsque vous .set() une nouvelle propriété, l'événement de change déclenche un nouveau rendu React, affichant de manière optimiste les nouvelles données. Lorsque vous essayez de le .save() sur le serveur, l'événement de request vous permet de savoir pour afficher une icône de chargement. Lorsque les choses se passent, l'événement de sync vous permet de savoir qu'il faut supprimer l'icône de chargement, ou l'événement error vous permet de savoir que les choses deviennent rouges. Vous pouvez voir l'inspiration ici.

Il existe également une validation (et un événement invalid correspondant) pour une première couche de défense, et une méthode .fetch() pour extraire de nouvelles informations du serveur.

Pour les tâches moins standard, interagir via ActionCreators peut avoir plus de sens. Je soupçonne que Facebook ne fait pas beaucoup de "simple CRUD", auquel cas il n'est pas surprenant qu'ils ne mettent pas les magasins en premier.

Conclusion

Les équipes d'ingénierie de Facebook ont ​​fait un travail remarquable pour faire avancer le Web frontal avec React, et l'introduction de Flux donne un aperçu d'une architecture plus large qui évolue vraiment : non seulement en termes de technologie, mais aussi d'ingénierie. Une utilisation intelligente et prudente de Backbone (selon l'exemple de ce didacticiel) peut combler les lacunes de Flux, ce qui permet à quiconque, des boutiques indépendantes individuelles aux grandes entreprises, de créer et de maintenir des applications impressionnantes.

En relation: Comment les composants React facilitent les tests d'interface utilisateur