Flujo de datos simple en aplicaciones React usando Flux y Backbone: un tutorial con ejemplos
Publicado: 2022-03-11React.js es una biblioteca fantástica. A veces parece lo mejor desde que se cortó Python. Sin embargo, React es solo una parte de una pila de aplicaciones front-end. No tiene mucho que ofrecer cuando se trata de administrar datos y estado.
Facebook, los creadores de React, han ofrecido alguna orientación allí en forma de Flux. Flux es una "arquitectura de aplicaciones" (no un marco) construida alrededor del flujo de datos unidireccional utilizando React Views, Action Dispatcher y Stores. El patrón Flux resuelve algunos problemas importantes al incorporar principios importantes de control de eventos, lo que hace que las aplicaciones de React sean mucho más fáciles de razonar, desarrollar y mantener.
Aquí, presentaré ejemplos básicos de flujo de control de Flux, discutiré lo que falta para las tiendas y cómo usar modelos y colecciones de backbone para llenar el vacío de una manera "compatible con Flux".
(Nota: utilizo CoffeeScript en mis ejemplos por conveniencia y brevedad. Los desarrolladores que no son CoffeeScript deberían poder seguirlo y tratar los ejemplos como pseudocódigo).
Introducción a Flux de Facebook
Backbone es una pequeña biblioteca excelente y bien examinada que incluye Vistas, Modelos, Colecciones y Rutas. Es una biblioteca estándar de facto para aplicaciones front-end estructuradas, y se ha combinado con las aplicaciones React desde que se introdujo esta última en 2013. La mayoría de los ejemplos de React fuera de Facebook.com hasta ahora han incluido menciones de que Backbone se usa en tándem.
Desafortunadamente, apoyarse solo en Backbone para manejar todo el flujo de la aplicación fuera de las vistas de React presenta complicaciones desafortunadas. Cuando comencé a trabajar por primera vez en el código de la aplicación React-Backbone, las "cadenas de eventos complejos" sobre las que había leído no tardaron en asomar sus cabezas de hidra. Enviar eventos desde la interfaz de usuario a los modelos, y luego de un modelo a otro y luego de regreso, hace que sea difícil realizar un seguimiento de quién estaba cambiando a quién, en qué orden y por qué.
Este tutorial de Flux demostrará cómo el patrón Flux maneja estos problemas con una facilidad y simplicidad impresionantes.
Una visión general
El eslogan de Flux es "flujo de datos unidireccional". Aquí hay un diagrama útil de los documentos de Flux que muestra cómo se ve ese flujo:
Lo importante es que las cosas fluyen desde React --> Dispatcher --> Stores --> React
.
Veamos cuáles son cada uno de los componentes principales y cómo se conectan:
Los documentos también ofrecen esta importante advertencia:
Flux es más un patrón que un marco, y no tiene dependencias estrictas. Sin embargo, a menudo usamos EventEmitter como base para las tiendas y React para nuestras vistas. La única pieza de Flux que no está disponible en otros lugares es el Dispatcher. Este módulo está disponible aquí para completar su caja de herramientas Flux.
Entonces Flux tiene tres componentes:
- Vistas (
React = require('react')
) - Despachador (
Dispatcher = require('flux').Dispatcher
) - Tiendas (
EventEmitter = require('events').EventEmitter
)- (o, como pronto veremos,
Backbone = require('backbone')
)
- (o, como pronto veremos,
Las vistas
No describiré React aquí, ya que se ha escrito mucho al respecto, aparte de decir que lo prefiero ampliamente a Angular. Casi nunca me siento confundido cuando escribo código React, a diferencia de Angular, pero, por supuesto, las opiniones varían.
el despachador
Flux Dispatcher es un lugar único donde se manejan todos los eventos que modifican sus tiendas. Para usarlo, cada tienda debe register
una sola devolución de llamada para manejar todos los eventos. Luego, cada vez que desee modificar una tienda, dispatch
un evento.
Al igual que React, Dispatcher me parece una buena idea, bien implementada. Como ejemplo, una aplicación que permite al usuario agregar elementos a una lista de tareas podría incluir lo siguiente:
# 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!"
Esto hace que sea muy fácil responder a dos preguntas:
- P: ¿Cuáles son todos los eventos que modifican
MyStore
?- R: Simplemente verifique los casos en la declaración de
switch
enMyStore.dispatchCallback
.
- R: Simplemente verifique los casos en la declaración de
- P: ¿Cuáles son todas las fuentes posibles de ese evento?
- R: Simplemente busque ese tipo de
actionType
.
- R: Simplemente busque ese tipo de
Esto es mucho más fácil que, por ejemplo, buscar MyModel.set
y MyModel.save
y MyCollection.add
, etc., donde rastrear las respuestas a estas preguntas básicas se vuelve realmente difícil, muy rápido.
Dispatcher también le permite hacer que las devoluciones de llamada se ejecuten secuencialmente de una manera simple y sincrónica, usando waitFor
. Por ejemplo:
# 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 la práctica, me sorprendió ver lo limpio que estaba mi código cuando usaba Dispatcher para modificar mis tiendas, incluso sin usar waitFor
.
Las tiendas
Entonces, los datos fluyen hacia las tiendas a través del Dispatcher. Entiendo. Pero, ¿cómo fluyen los datos de las tiendas a las vistas (es decir, React)? Como se indica en los documentos de Flux:
[La] vista escucha los eventos que transmiten las tiendas de las que depende.
Ok genial. Al igual que registramos devoluciones de llamada con nuestras tiendas, registramos devoluciones de llamada con nuestras vistas (que son componentes de React). Le decimos a React que vuelva a render
cada vez que ocurra un cambio en la Tienda que se pasó a través de sus props
.
Por ejemplo:
# 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
¡Increíble!
Entonces, ¿cómo emitimos ese evento de "change"
? Bueno, Flux recomienda usar EventEmitter
. De un ejemplo oficial:
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...
¡Bruto! ¿Tengo que escribir todo eso yo mismo, cada vez que quiero una tienda simple? ¿Cuál se supone que debo usar cada vez que tengo una información que quiero mostrar? ¡Tiene que haber una mejor manera!
la pieza que falta
Los modelos y colecciones de Backbone ya tienen todo lo que parecen estar haciendo las tiendas basadas en EventEmitter de Flux.

Al indicarle que use EventEmitter sin formato, Flux recomienda que recree entre el 50 y el 75 % de los modelos y colecciones de Backbone cada vez que cree una tienda. Usar EventEmitter para sus tiendas es como usar Node.js solo para su servidor cuando ya existen microframeworks bien construidos como Express.js o equivalentes para encargarse de todos los aspectos básicos y repetitivos.
Al igual que Express.js se basa en Node.js, los modelos y las colecciones de Backbone se basan en EventEmitter. Y tiene todo lo que casi siempre necesita: Backbone emite eventos de change
y tiene métodos de consulta, getters y setters y todo. Además, Jeremy Ashkenas de Backbone y su ejército de 230 colaboradores hicieron un trabajo mucho mejor en todas esas cosas de lo que probablemente pueda hacer yo.
Como ejemplo para este tutorial de Backbone, convertí el ejemplo de MessageStore de arriba a una versión de Backbone.
Es objetivamente menos código (no es necesario duplicar el trabajo) y es subjetivamente más claro y conciso (por ejemplo, this.add(message)
en lugar de _messages[message.id] = message
).
¡Así que usemos Backbone para tiendas!
El patrón FluxBone: Flux Stores de Backbone
Este tutorial es la base de un enfoque que con orgullo he denominado FluxBone , la arquitectura Flux que utiliza Backbone para tiendas. Aquí está el patrón básico de una arquitectura FluxBone:
- Las tiendas son colecciones o modelos troncales instanciados, que han registrado una devolución de llamada con el despachador. Por lo general, esto significa que son singletons.
- Los componentes de vista nunca modifican directamente las tiendas (por ejemplo, no
.set()
). En su lugar, los componentes envían acciones al despachador. - Los componentes de la vista consultan las tiendas y vinculan sus eventos para desencadenar actualizaciones.
Usemos ejemplos de Backbone y Flux para ver cada parte de eso a la vez:
1. Las tiendas son Modelos o Colecciones de Backbone instanciados, que han registrado una devolución de llamada con el Despachador.
# 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. Los componentes nunca modifican directamente las tiendas (por ejemplo, no .set()
). En su lugar, los componentes envían acciones al despachador.
# 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. Los componentes consultan las tiendas y se unen a sus eventos para desencadenar actualizaciones.
# 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
He aplicado este enfoque Flux and Backbone a mis propios proyectos, y una vez que rediseñé mi aplicación React para usar este patrón, casi todas las partes desagradables desaparecieron. Fue un poco milagroso: uno por uno, los fragmentos de código que me hacían rechinar los dientes buscando una mejor manera fueron reemplazados por un flujo sensato. Y la suavidad con la que Backbone parece integrarse en este patrón es notable: no siento que esté luchando contra Backbone, Flux o React para encajarlos en una sola aplicación.
Ejemplo de mezcla
Escribir el this.on(...)
y this.off(...)
cada vez que agrega una tienda FluxBone a un componente puede volverse un poco viejo.
Aquí hay un ejemplo de React Mixin que, aunque es extremadamente ingenuo, sin duda haría que la iteración sea aún más fácil:
# 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") )
Sincronización con una API web
En el diagrama Flux original, interactúa con la API web solo a través de ActionCreators, que requieren una respuesta del servidor antes de enviar acciones al Dispatcher. Eso nunca me sentó bien; ¿No debería ser la Tienda la primera en enterarse de los cambios, antes que el servidor?
Elijo darle la vuelta a esa parte del diagrama: las tiendas interactúan directamente con una API RESTful CRUD a través de la sync()
de Backbone. Esto es maravillosamente conveniente, al menos si está trabajando con una API RESTful CRUD real.
La integridad de los datos se mantiene sin problemas. Cuando .set()
una nueva propiedad, el evento de change
desencadena una nueva representación de React, mostrando de manera optimista los nuevos datos. Cuando intenta .save()
en el servidor, el evento de request
le permite saber que debe mostrar un icono de carga. Cuando las cosas funcionan, el evento de sync
le permite saber si debe eliminar el ícono de carga, o el evento de error
le permite saber si desea cambiar las cosas a rojo. Puedes ver la inspiración aquí.
También hay validación (y un evento invalid
correspondiente) para una primera capa de defensa y un método .fetch()
para extraer nueva información del servidor.
Para tareas menos estándar, interactuar a través de ActionCreators puede tener más sentido. Sospecho que Facebook no hace mucho "simple CRUD", en cuyo caso no es de extrañar que no pongan las Tiendas en primer lugar.
Conclusión
Los equipos de ingeniería de Facebook han realizado un trabajo notable para impulsar la interfaz web con React, y la introducción de Flux ofrece un vistazo a una arquitectura más amplia que realmente escala: no solo en términos de tecnología, sino también de ingeniería. El uso inteligente y cuidadoso de Backbone (según el ejemplo de este tutorial) puede llenar los vacíos en Flux, lo que hace que sea increíblemente fácil para cualquiera, desde tiendas independientes de una sola persona hasta grandes empresas, crear y mantener aplicaciones impresionantes.