A Tutorial with Examples on Implementing Simple Data Flow in React Apps Using Flux and Backbone

React.js is a phenomenal library, often considered a game-changer in the Python world. However, React only addresses the view layer of a front-end application and lacks built-in solutions for data and state management.

To address this, Facebook, the creators of React, introduced offered some guidance in the form of Flux. Flux, more of an application architecture than a framework, centers around unidirectional data flow using React Views, an Action Dispatcher, and Stores. By adhering to event control principles, Flux simplifies the development, reasoning, and maintenance of React applications.

This article delves into basic Flux control flow examples, highlights the limitations of Stores, and explores how to integrate Backbone Models and Collections seamlessly within a “Flux-compliant” manner.

(Note: For brevity and clarity, the examples are presented in CoffeeScript. Non-CoffeeScript developers can treat them as pseudocode.)

Introduction to Facebook’s Flux

Backbone is a highly regarded library that provides Views, Models, Collections, and Routes, establishing itself as a standard for structured front-end applications. Since React’s debut in 2013, Backbone has often been used alongside it. Most React examples outside of Facebook.com showcase this tandem.

However, relying solely on Backbone for application flow beyond React Views can introduce complexities. In my initial experiences with React-Backbone applications, I quickly encountered the infamous “complex event chains” that I had read about. The intricate web of events, from UI to Models and back, made it challenging to trace data modifications and their origins.

This Flux tutorial will showcase how the Flux pattern addresses these issues with elegance and simplicity.

An Overview

Flux’s core principle is “unidirectional data flow”. The following handy diagram illustrates this flow:

Facebook Flux uses an “unidirectional data flow” model that varies a little when paired with React and Backbone.

The key takeaway is the unidirectional flow: React --> Dispatcher --> Stores --> React.

Let’s examine the main components and their interactions:

The official documentation provides an important clarification:

Flux is more of a pattern than a framework, and does not have any hard dependencies. However, we often use EventEmitter as a basis for Stores and React for our Views. The one piece of Flux not readily available elsewhere is the Dispatcher. This module is available here to complete your Flux toolbox.

Therefore, Flux comprises three core components:

  1. Views (React = require('react'))
  2. Dispatcher (Dispatcher = require('flux').Dispatcher)
  3. Stores (EventEmitter = require('events').EventEmitter)
    • (or, as we’ll soon explore, Backbone = require('backbone'))

The Views

Given the abundance of resources on React, I’ll refrain from a detailed explanation here, except to say that I find it significantly more intuitive than Angular. Unlike Angular, React rarely leaves me perplexed while coding. However, this is subjective.

The Dispatcher

The Flux Dispatcher serves as a central hub for handling all events that modify Stores. Each Store registers a single callback with the Dispatcher to handle all events. Subsequently, whenever a Store needs modification, an event is dispatched.

Similar to React, the Dispatcher stands out as a well-implemented, sound concept. For instance, consider a to-do list application where users can add items. Here’s how the Dispatcher could be utilized:

1
2
3
4
5
6
# in TodoDispatcher.coffee
Dispatcher = require("flux").Dispatcher

TodoDispatcher = new Dispatcher() # That's all it takes!.

module.exports = TodoDispatcher    
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 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!"

This approach simplifies the process of answering two crucial questions:

  1. Q: What events modify MyStore?
    • A: Refer to the switch statement cases within MyStore.dispatchCallback.
  2. Q: What are the potential sources of that event?
    • A: Simply search for the specific actionType.

This method is far more efficient than, for example, searching for MyModel.set, MyModel.save, MyCollection.add, and so on, which can quickly become unwieldy.

The Dispatcher also enables sequential execution of callbacks synchronously using waitFor. For instance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 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

I was genuinely surprised by the significant improvement in code cleanliness achieved by using the Dispatcher for Store modifications, even without leveraging waitFor.

The Stores

While we understand that data flows into Stores via the Dispatcher, how does it reach the Views (i.e., React)? As stated in the Flux docs:

[The] view listens for events that are broadcast by the stores that it depends on.

Essentially, we register callbacks with our Views (React Components), similar to how we did with Stores. We instruct React to re-render whenever a change occurs in the Store passed through its props.

Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 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

Excellent!

Now, how do we emit the "change" event? Flux recommends utilizing EventEmitter. Let’s consider an official example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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...

This feels cumbersome! Do we have to write all this boilerplate code every time we need a simple Store, which is seemingly required for every piece of information we want to display? There must be a better way!

The Missing Piece

Backbone’s Models and Collections already encompass the functionality provided by Flux’s EventEmitter-based Stores.

By recommending raw EventEmitter, Flux essentially suggests recreating a significant portion of Backbone’s Models & Collections for every Store. This is akin to using bare Node.js for server-side development when mature microframeworks like Express.js already exist to handle the basics.

Just as Express.js builds upon Node.js, Backbone’s Models and Collections utilize EventEmitter as their foundation. They offer all the essential features, including change event emission, query methods, getters, setters, and more. Furthermore, Backbone’s Jeremy Ashkenas and his team of 230 contributors have undoubtedly implemented these functionalities more robustly than most of us could.

To illustrate, I’ve converted the MessageStore example above to utilize a Backbone version:

The code is objectively shorter (no redundant code) and subjectively clearer and more concise (e.g., this.add(message) instead of _messages[message.id] = message).

Therefore, let’s leverage Backbone for our Stores!

The FluxBone Pattern: Flux Stores by Backbone

This approach forms the basis of what I call FluxBone, a Flux architecture that employs Backbone for Stores. Here’s the fundamental pattern:

  1. Stores are instantiated as Backbone Models or Collections that have registered a callback with the Dispatcher, typically as singletons.
  2. View components never directly modify Stores (e.g., no .set() calls). Instead, they dispatch Actions to the Dispatcher.
  3. View components query Stores and bind to their events to trigger updates.
This Backbone tutorial is designed to look at the way Backbone and Flux work together in React applications.

Let’s break down each step with Backbone and Flux examples:

1. Stores are instantiated as Backbone Models or Collections, registered with the Dispatcher.

1
2
3
4
5
6
# in TodoDispatcher.coffee
Dispatcher = require("flux").Dispatcher

TodoDispatcher = new Dispatcher() # That's all it takes!

module.exports = TodoDispatcher
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 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. Components dispatch Actions, not direct Store modifications.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 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. Components query Stores, bind to events for updates.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 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

Having implemented this Flux-Backbone approach in my projects, I witnessed a remarkable transformation after refactoring my React application. The code sections that previously caused frustration were replaced by a logical and streamlined flow. The seamless integration of Backbone into this pattern is truly impressive, eliminating any friction between Backbone, Flux, and React.

Example Mixin

Repeatedly writing this.on(...) and this.off(...) for every FluxBone Store added to a component can become tedious.

Here’s a simple React Mixin that can streamline this process:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# in FluxBoneMixin.coffee
module.exports = (propName) ->
  componentDidMount: ->
    @props[propName].on "all", =>
      @forceUpdate()
    , @

  componentWillUnmount: ->
    @props[propName].off "all", =>
      @forceUpdate()
    , @
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 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")
)

Syncing with a Web API

The original Flux diagram depicts interactions with the Web API solely through ActionCreators, requiring a server response before dispatching actions. However, shouldn’t the Store be the first to know about changes, even before the server?

I propose reversing this flow: Stores interact directly with a RESTful CRUD API using Backbone’s sync() method. This proves highly convenient, especially when working with a true RESTful CRUD API.

Data integrity remains intact. When .set() is called with a new property, the change event triggers a React re-render, optimistically displaying the new data. During .save() to the server, the request event prompts a loading indicator. Upon successful completion, the sync event removes the indicator, while the error event highlights issues. You can find inspiration here.

Furthermore, Backbone provides validation (and an invalid event) as a first line of defense and a .fetch() method to retrieve updated information from the server.

For non-standard tasks, interacting via ActionCreators might be more suitable. It’s plausible that Facebook doesn’t rely heavily on “mere CRUD,” explaining their preference for not prioritizing Stores.

Conclusion

Facebook’s Engineering teams have made significant contributions to advancing front-end development with React. Flux provides a glimpse into a scalable architecture, both technologically and from an engineering perspective. By cleverly integrating Backbone, as demonstrated in this tutorial, Flux’s capabilities can be enhanced, empowering developers and teams of all sizes to build and maintain impressive applications.

Licensed under CC BY-NC-SA 4.0