Fluxo de dados simples em aplicativos React usando fluxo e backbone: um tutorial com exemplos
Publicados: 2022-03-11React.js é uma biblioteca fantástica. Às vezes parece ser a melhor coisa desde o Python fatiado. React é apenas uma parte de uma pilha de aplicativos front-end, no entanto. Ele não tem muito a oferecer quando se trata de gerenciamento de dados e estado.
O Facebook, os criadores do React, ofereceram algumas orientações na forma do Flux. Flux é uma “Arquitetura de Aplicativo” (não uma estrutura) construída em torno de fluxo de dados unidirecional usando React Views, um Action Dispatcher e Stores. O padrão Flux resolve alguns dos principais problemas incorporando princípios importantes de controle de eventos, que tornam os aplicativos React muito mais fáceis de raciocinar, desenvolver e manter.
Aqui, apresentarei exemplos básicos de fluxo de controle de fluxo, discutirei o que está faltando para lojas e como usar modelos e coleções de backbone para preencher a lacuna de uma maneira “compatível com fluxo”.
(Observação: eu uso CoffeeScript em meus exemplos por conveniência e brevidade. Desenvolvedores não-CoffeeScript devem ser capazes de acompanhar e podem tratar os exemplos como pseudocódigo.)
Introdução ao fluxo do Facebook
O Backbone é uma pequena biblioteca excelente e bem avaliada que inclui Views, Models, Collections e Routes. É uma biblioteca padrão de fato para aplicativos front-end estruturados, e foi emparelhado com aplicativos React desde que o último foi introduzido em 2013. A maioria dos exemplos de React fora do Facebook.com até agora incluiu menções ao Backbone sendo usado em conjunto.
Infelizmente, apoiar-se apenas no Backbone para lidar com todo o fluxo da aplicação fora das Views do React apresenta complicações infelizes. Quando comecei a trabalhar no código do aplicativo React-Backbone, as “cadeias de eventos complexas” sobre as quais eu tinha lido não demoraram muito para criar suas cabeças de hidra. Enviar eventos da interface do usuário para os modelos e, em seguida, de um modelo para outro e vice-versa, dificulta o acompanhamento de quem estava alterando quem, em que ordem e por quê.
Este tutorial do Flux demonstrará como o padrão Flux lida com esses problemas com facilidade e simplicidade impressionantes.
Uma visão geral
O slogan da Flux é “fluxo de dados unidirecional”. Aqui está um diagrama útil dos documentos do Flux mostrando como é esse fluxo:
A parte importante é que as coisas fluem de React --> Dispatcher --> Stores --> React
.
Vejamos o que são cada um dos principais componentes e como eles se conectam:
Os documentos também oferecem esta importante advertência:
O Flux é mais um padrão do que uma estrutura e não possui dependências rígidas. No entanto, muitas vezes usamos EventEmitter como base para Stores e React para nossas Views. A única peça do Flux que não está prontamente disponível em outros lugares é o Dispatcher. Este módulo está disponível aqui para completar sua caixa de ferramentas Flux.
Então o Flux tem três componentes:
- Visualizações (
React = require('react')
) - Despachante (
Dispatcher = require('flux').Dispatcher
) - Armazena (
EventEmitter = require('events').EventEmitter
)- (ou, como veremos em breve,
Backbone = require('backbone')
)
- (ou, como veremos em breve,
As visualizações
Não vou descrever o React aqui, já que muito já foi escrito sobre ele, além de dizer que prefiro muito mais do que Angular. Quase nunca me sinto confuso ao escrever código React, ao contrário do Angular, mas é claro que as opiniões variam.
O Despachante
O Flux Dispatcher é um local único onde são tratados todos os eventos que modificam suas Lojas. Para usá-lo, você faz com que cada Loja register
um único retorno de chamada para lidar com todos os eventos. Então, sempre que você quiser modificar uma Loja, você dispatch
um evento.
Assim como o React, o Dispatcher me parece uma boa ideia, bem implementado. Por exemplo, um aplicativo que permite ao usuário adicionar itens a uma lista de tarefas pode incluir o seguinte:
# 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!"
Isso torna muito fácil responder a duas perguntas:
- P: Quais são todos os eventos que modificam a
MyStore
?- R: Basta verificar os casos na instrução
switch
emMyStore.dispatchCallback
.
- R: Basta verificar os casos na instrução
- P: Quais são todas as fontes possíveis desse evento?
- R: Basta pesquisar por esse
actionType
.
- R: Basta pesquisar por esse
Isso é muito mais fácil do que, por exemplo, procurar por MyModel.set
e MyModel.save
e MyCollection.add
etc, onde rastrear as respostas para essas perguntas básicas fica muito difícil muito rápido.
O Dispatcher também permite que você execute callbacks sequencialmente de maneira simples e síncrona, usando waitFor
. Por exemplo:
# 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
Na prática, fiquei chocado ao ver o quanto meu código ficou mais limpo ao usar o Dispatcher para modificar minhas Stores, mesmo sem usar waitFor
.
As lojas
Assim, os dados fluem para as Lojas por meio do Dispatcher. Entendi. Mas como os dados fluem das Stores para as Views (ou seja, React)? Conforme declarado nos documentos do Flux:
[A] visualização escuta eventos que são transmitidos pelas lojas das quais ela depende.
Certo, ótimo. Assim como registramos callbacks com nossas Stores, registramos callbacks com nossas Views (que são React Components). Dizemos ao React para render
sempre que ocorrer uma mudança na Store que foi passada através de seus props
.
Por exemplo:
# 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
Impressionante!
Então, como emitimos esse evento de "change"
? Bem, a Flux recomenda usar EventEmitter
. De um exemplo 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! Eu tenho que escrever tudo isso sozinho, toda vez que quero uma loja simples? Qual devo usar toda vez que tiver uma informação que quero exibir? Tem que haver uma maneira melhor!
A peça que faltava
Os modelos e coleções da Backbone já têm tudo o que as lojas baseadas em EventEmitter da Flux parecem estar fazendo.
Ao dizer para você usar o EventEmitter bruto, a Flux recomenda que você recrie talvez 50-75% dos modelos e coleções do Backbone toda vez que criar uma loja. Usar EventEmitter para suas lojas é como usar Node.js simples para seu servidor quando microframeworks bem construídos como Express.js ou equivalentes já existem para cuidar de todos os fundamentos e clichês.

Assim como o Express.js é construído no Node.js, os Modelos e Coleções do Backbone são construídos no EventEmitter. E tem todas as coisas que você quase sempre precisa: Backbone emite eventos de change
e tem métodos de consulta, getters e setters e tudo mais. Além disso, Jeremy Ashkenas, da Backbone, e seu exército de 230 colaboradores fizeram um trabalho muito melhor em todas essas coisas do que eu provavelmente seria capaz de fazer.
Como exemplo para este tutorial do Backbone, converti o exemplo MessageStore acima para uma versão do Backbone.
É objetivamente menos código (sem necessidade de duplicar o trabalho) e é subjetivamente mais claro e conciso (por exemplo, this.add(message)
em vez de _messages[message.id] = message
).
Então vamos usar o Backbone for Stores!
O padrão FluxBone: Flux Stores por Backbone
Este tutorial é a base de uma abordagem que orgulhosamente apelidei de FluxBone , a arquitetura Flux usando Backbone for Stores. Aqui está o padrão básico de uma arquitetura FluxBone:
- Stores são Modelos ou Coleções de Backbone instanciados, que registraram um retorno de chamada com o Dispatcher. Normalmente, isso significa que eles são singletons.
- Os componentes de visualização nunca modificam diretamente as Lojas (por exemplo, no
.set()
). Em vez disso, os componentes despacham Actions para o Dispatcher. - Exibir componentes consulta Stores e vincular a seus eventos para acionar atualizações.
Vamos usar exemplos de Backbone e Flux para analisar cada parte disso:
1. Lojas são Modelos ou Coleções de Backbone instanciados, que registraram um retorno de chamada com o Dispatcher.
# 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. Componentes nunca modificam diretamente Stores (por exemplo, no .set()
). Em vez disso, os componentes despacham Actions para o Dispatcher.
# 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. Componentes consultam Stores e se associam a seus eventos para acionar atualizações.
# 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
Eu apliquei essa abordagem de Flux e Backbone em meus próprios projetos e, uma vez que re-arquitetei meu aplicativo React para usar esse padrão, quase todos os bits feios desapareceram. Foi um pouco milagroso: um por um, os pedaços de código que me fizeram ranger os dentes procurando uma maneira melhor foram substituídos por um fluxo sensato. E a suavidade com que o Backbone parece se integrar nesse padrão é notável: não sinto que estou lutando contra Backbone, Flux ou React para encaixá-los em um único aplicativo.
Exemplo de Mixin
Escrever o this.on(...)
e this.off(...)
toda vez que você adiciona um FluxBone Store a um componente pode ficar um pouco antigo.
Aqui está um exemplo do React Mixin que, embora extremamente ingênuo, certamente tornaria a iteração rápida ainda mais 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") )
Sincronizando com uma API da Web
No diagrama de fluxo original, você interage com a API da Web apenas por meio de ActionCreators, que exigem uma resposta do servidor antes de enviar ações para o Dispatcher. Isso nunca caiu bem comigo; a Loja não deveria ser a primeira a saber das mudanças, antes do servidor?
Eu escolho inverter essa parte do diagrama: as Stores interagem diretamente com uma API RESTful CRUD através do sync()
do Backbone. Isso é maravilhosamente conveniente, pelo menos se você estiver trabalhando com uma API RESTful CRUD real.
A integridade dos dados é mantida sem problemas. Quando você .set()
uma nova propriedade, o evento change
aciona uma nova renderização do React, exibindo de forma otimista os novos dados. Quando você tenta .save()
no servidor, o evento de request
permite que você exiba um ícone de carregamento. Quando as coisas acontecem, o evento de sync
permite que você remova o ícone de carregamento ou o evento de error
permite que você deixe as coisas vermelhas. Você pode ver a inspiração aqui.
Há também validação (e um evento invalid
correspondente) para uma primeira camada de defesa e um método .fetch()
para extrair novas informações do servidor.
Para tarefas menos padrão, interagir via ActionCreators pode fazer mais sentido. Suspeito que o Facebook não faça muito “mero CRUD”, caso em que não é de surpreender que eles não coloquem as Lojas em primeiro lugar.
Conclusão
As equipes de engenharia do Facebook fizeram um trabalho notável para impulsionar a web de front-end com o React, e a introdução do Flux dá uma olhada em uma arquitetura mais ampla que realmente escala: não apenas em termos de tecnologia, mas também de engenharia. O uso inteligente e cuidadoso do Backbone (de acordo com o exemplo deste tutorial) pode preencher as lacunas do Flux, tornando incrivelmente fácil para qualquer pessoa, desde lojas independentes de uma pessoa até grandes empresas, criar e manter aplicativos impressionantes.