Kapost Engineering

Recent Posts


Recent Comments


Archives


Categories


Meta


Transitioning to Flux Architecture

Nathanael BeisiegelNathanael Beisiegel

After trying several front-end view frameworks, including Backbone views and Knockout.js, our team has decided to invest in React. While it will be a long transition to convert all of our legacy views, we’ve already seen a huge jump in productivity and quality on the front-end.

In our transition to React, we figured we would need to build something like Flux eventually but could get by with our existing Backbone models. However, we encountered pain points with data flow and synchronization more quickly then we thought. We decided to take a stab at Flux and were pleasantly surprised with the speed and ease we had implementing the architecture.

I’m going to share how we transitioned to Flux and give some insight into why each part of the architecture is valuable. I’ll also show a couple examples similar to real problems we faced here at Kapost. I do assume the reader has some knowledge of React, a rough idea of what Flux is, and doesn’t mind reading a lot of code. If you are unfamiliar with React and Flux, check out the React documentation and the basic introduction to Flux. (Unfortunately the Flux docs are like a shoddy professor—giving a vague lecture and basic example, then running off and expecting you to solve the hard problems with no office hours.) I’ll try to explain the parts in more detail below.

(Note that I am using CommonJS modules in these examples. Flux is just a pattern, so you can replace these with whatever you would like, such as ES6 imports, Require.js, etc.)

Data Hierarchy

For the first example, we’ll build a component very similar to one we recently built at Kapost. Let’s say we own a business that provides simple domain registration and DNS hosting exclusively for emoji web addresses. We want to transition our static forms to a slick, single page React app that looks something like the following.

EmojiDomains.biz Mock

EmojiDomains.biz Mock

We have a list of domains that we can edit and save in place with a dropdown. Each domain has a set of fields including a list of subdomains that all point to different IP addresses. When we load the page, we need to show a loading state until the list of domains is returned. When we hit save, we want to post the individual record to the server and let the user know if successful, or keep the panel open to try saving again.

We split the UI into the components into the following:

UI Split into React Components

UI Split into React Components

We can imagine the JSON representing this list to look something like the following:


{
  "response": [
    {
      "domain": "🔥.engineering",
      "expire": "2w",
      "ttl": "3h",
      "a_records": [
        {
          "subdomain": "@",
          "address": "8.8.4.4"
        },
        ...
      ]
    },
    ...
  ]
}

Note the nested data structure—we have a list of domains that each have their own list of A Records. Let’s start building!

Without Flux

Naively, we might just start building this will React alone. Hey, no need to over-engineer if it works, right?

// DomainList.js.jsx
var DomainList = React.createClass({
  componentDidMount: function() {
    some.ajax.library('api/domains/index', this.handleSuccess, this.handleFailure);
  },

  getInitialState: function() {
    return {
      items: null,
      loading: 'initial'
    };
  },

  render: function() {
    if (this.state.loading == 'initial') {
      return <p>Loading...</p>
    }
    else if (this.state.loading == 'failed') {
      return <p>Failed...</p>
    }
    else {
      return <div>{this.state.items.map(this.renderDomainItem)}</div>
    }
  },

  renderDomainItem: function(item) {
    return <DomainItem key={item.id} item={item} />
  },

  handleSuccess: function(items) {
    this.setState({ items: items, loading: 'success' });
  },

  handleFailure: function() {
    this.setState({ loading: 'failed' });
  }
});

Not horrible. But now children must update the parent’s nested data structure through a callback with arguments referencing where it lives in the data structure. That means sending down keys and indexes for each section of the structure. For example, editing the IP address for the 👾👾👾.games address would at minimum require a updating callback with the domain index and record table row index.

// DomainList.js.jsx
updateIpRecord: function(domainIndex, recordItemIndex, newData) { /* ... */ },

renderDomainItem: function(item, index) {
  return <DomainItem key={item.id}
                     item={item}
                     domainIndex={index}
                     updateIpRecord={this.updateIpRecord} />
}

// DomainItem.js.jsx
render: function() {
  <div>
    // ... other form fields
    <table>
      {this.props.item.records.map(this.renderIpRow)}
    </table>
  </div>
},

renderIpRow: function(recordItem, index) {
  return <IpRow key={recordItem.id}
                {...this.props} // Send down domainIndex and update callback
                recordItem={recordItem}
                recordIndex={index} />
}

// IpRow.js.jsx
onFormChangeHandler: function(event) {
  // ... parse form data to update
  this.props.updateIpRecord(this.props.domainIndex, this.props.recordIndex, newData)
}

This doesn’t scale well. Callbacks become very specific and components are very tightly coupled as you have to modify deep parts of the object at the bottom of the hierarchy. If we had another table to edit (say CNAME records) we would then need to be even more specific with our callbacks.

Another limitation is that all components that need to share this state must be encapsulated by a parent component. In our example, we wanted to have a counter with the number of domains at the top. We would also need to render the Counter component inside a common parent with the DomainList component. With large apps and complicated views, all your state starts living in a parent, essentially becoming global.

You may be thinking that this wouldn’t be nearly as bad if we could edit objects by reference and avoid the need for deep callbacks. We tried this by using our existing Backbone models and collections and sending those down as props on each component. The idea was that these were objects that could be saved directly with no need to reference the parent.

// DomainList.js.jsx
propTypes: {
  domainCollection: React.PropTypes.instanceOf(Backbone.Collection)
},

renderDomainItem: function(domainModel) {
  return <DomainItem key={item.id} model={domainModel} />
}

// DomainItem.js.jsx
var DomainItem = React.createClass({
  propTypes: {
    model: React.PropTypes.instanceOf(Backbone.Model)
  },

  componentDidMount: function() {
    var _this = this;
    this.props.model.on('change', function(data) {
      _this.setState(data);
    });
  },

  componentWillUnmount: function() {
    this.props.model.off('change')
  },

  render: function() { /* ... */ },

  onChangeHandler: function() {
    // ...
    this.props.model.set(newData) // Bummer: doesn't trigger update event on parent collection
  }
});

The weakness in the approach is that you have to completely tie yourself to the Backbone event system and manually synchronize the component’s state with the model. Even in this basic example it’s tricky to know if we should update from the model in the parent or child. The Backbone event system also proved to be too limited to effectively update in all cases. One major issue we ran into was the lack of events on the parent collection when a child model was saved.

By violating the one-way data flow pattern, we were losing most of the benefits of React and ended up with a bunch of code trying to keep state in sync. At this point, we realized we needed a different pattern.

Let’s Use Flux!

In case you are still a bit confused with the Flux pattern, let me give you a basic review of all the pieces.

Flux is not much more than a fancy event system around Stores. Stores are the most essential piece, which are singletons that simply hold data. Stores allow us to hold state outside of the React components, such as the above list of domains. Any component that needs this data (DomainList.js.jsx, Counter.js.jsx, etc) can reference the singleton and subscribe to updates to update state.

What if we only used stores and components, without using any other parts of Flux? It might look something like the following:

Not Really Flux

Not Really Flux

// DomainList.js.jsx
var DomainStore = require('../DomainStore');

var DomainList = React.createClass({
  componentDidMount: function() {
    DomainStore.addChangeListener(this.onStoreChange);
    DomainStore.fetchDataIfNecessary();
  },

  onStoreChange: function(data) {
    this.setState(DataStore.data());
  },

  getInitialState: function(data) {
    return DomainStore.data();
  }

  // ...


// DomainStore.js
var DomainStore = {
  addChangeListener: function(callback) {
    // Store and register callback
  },

  emitChange: function() {
    // Run all registered callbacks
  },

  data: function() {
    return this._data;
  },

  initialize: function() {
    this._data = { loadingState: 'loading', domains: [] }
    some.ajax.library('api/domains/index', this.handleSuccess, this.handleFailure);
  },

  handleSuccess: function(data) {
    this._data.domains = data;
    this._data.loadingState = 'loaded';
    this.emitChange();
  },

  // ... many other methods that you create to update the
  //     underlying data collection then emit change
}

DomainStore.initialize();

module.exports = DomainStore;

This nicely separates state out of the component. We are able to use this singleton in several components, and update whenever the store emits a change. It’s pretty close to Backbone, but you have more control on the emitted events. And all those updating callbacks? They are simply methods on the store, and you can use as many as you need without the hassle of passing them all down. You will still need references in the child components (ids, keys, and indices) to dig into the store’s data structure, but you can add as many functions as you need with no need to pass them down as props.

So why bother with actions and the dispatcher? They buy you a few nice things. One is the magical one-way data flow that give you bragging points. More importantly, they can take the complexity of API calls out of the store and allow multiple stores respond to the same actions. The component can then simply rely on events and not care about what state the store is in.

Basic Flux Diagram

Basic Flux Diagram

We are using Facebook’s dispatcher, which is a singleton that broadcasts events and data to all subscribers. Your entire app should use the same dispatcher to allow stores to subscribe any events in your app.

So finally, we end up with something that looks like this:

// --------------------------------------------------------------------------
// DomainList.js.jsx
//
// The parent component only need to kick off the store if necessary
// and update state on store change
// --------------------------------------------------------------------------
var DomainStore = require('../DomainStore');
var DomainActions = require('../DomainActions');

var DomainList = React.createClass({
  componentDidMount: function() {
    DomainStore.addChangeListener(this.onStoreChange);
    DomainActions.fetchDataIfNecessary();
  },

  onStoreChange: function(data) {
    this.setState(DataStore.data());
  },

  getInitialState: function(data) {
    return DomainStore.data();
  },

  render: function() {
    return <div>{this.state.domains.map(this.renderDomainItem)}</div>
  },

  // ...
}

// --------------------------------------------------------------------------
// IpRow.js.jsx
//
// This nested component can update the parent by including actions as well!
// Any updates will trigger an update in the store, which will update the
// parent component and flow down to the individual IpRow.
// --------------------------------------------------------------------------
var DomainActions = require('../DomainActions');

var IpRow = React.createClass({
  // ...

  onFormChangeHandler: function(event) {
    DomainActions.updateIpRow(this.props.domainIndex, this.props.rowIndex, newData);
  }
}

// --------------------------------------------------------------------------
// DomainActions.js
//
// Actions now handle the API calls and fire different events to the store.
// It's nice to mirror AJAX events as dispatcher events.
// --------------------------------------------------------------------------
var dispatcher = require('dispatcher');
var DomainStore = require('../DomainStore');

module.exports = {
  fetchDataIfNecessary: function() {
    loadingState = DomainStore.data().loadingState;
    if (loadingState === 'initial' || loadingState === 'failed' ) {
      some.ajax.library('api/domains/index', this.handleSuccess, this.handleFailure);
    }
  },

  handleSuccess: function(newData) {
    dispatcher.dispatch({
      type: 'DOMAIN_FETCH_SUCCESS',
      data: newData
    });
  },

  handleFailure: function() {
    dispatcher.dispatch({
      type: 'DOMAIN_FETCH_FAILURE'
    });
  },

  updateIpRow: function(domainIndex, rowIndex, newData) {
    dispatcher.dispatch({
      type: 'DOMAIN_UPDATE_IP_ROW',
      domainIndex: domainIndex,
      rowIndex: rowIndex,
      data: newData
    });
  }

  // Other actions to update as necessary
}

// --------------------------------------------------------------------------
// DomainStore.js
//
// The store is nice and clean now, only concerned with manipulating data
// and emitting changes to any listening components.
// --------------------------------------------------------------------------
var DomainStore = {
  addChangeListener: function(callback) {
    // Store and register callback
  },

  emitChange: function() {
    // Run all registered callbacks
  },

  data: function() {
    return this._data;
  },

  initialize: function() {
    this._data = { loadingState: 'initial', domains: [] }
  },

  updateOnSuccess: function(data) {
    this._data.domains = data;
    this._data.loadingState = 'loaded';
    this.emitChange();
  },

  updateOnFailure: function() {
    this._data.loadingState = 'failed';
    this.emitChange();
  },

  // ...
}

DomainStore.initialize();

// We store the dispatchToken in case we want to order callbacks
// with the dispatcher's `waitFor` method. We register to different
// events and respond accordingly.
DomainStore.dispatchToken = dispatcher.register(function(payload) {
  var action = payload.action;

  switch (action.actionType) {
    case 'DOMAIN_FETCH_SUCCESS':
      DomainStore.updateOnSuccess(action.data);
      break;
    case 'DOMAIN_FETCH_FAILURE':
      DomainStore.updateOnFailure();
      break;
    case 'DOMAIN_UPDATE_IP_ROW':
      DomainStore.updateIpRow(action.domainIndex, action.rowIndex, action.data);
      break;
    default:
      return;
  }
});

module.exports = DomainStore;

Very nice! We have achieved one-way data flow. As you start working with this pattern, you’ll notice that it’s obvious where to put new code, and that the complexity of loading and updating through API calls is nicely hidden behind the event system. To get a better idea how this looks, checkout the Chat repository from Facebook. It includes constants and other things I have excluded for brevity.

Dependent Stores

So what about data stores that are dependent on another to be loaded in advance? For the final example, let’s make something incredibly common to all apps: a Comment component.

A Sad Comment Thread

A Sad Comment Thread

The component should be trivial to create, but it relies on user information in the thread. Unfortunately, our comments API only returns normalized data with a user_id.

{
  "response": [
    {
      "body": "Hey no one seems to be able to find our site with these domains.",
      "user_id": "5554ff03acf6ae14e1000005",
      ...
    },
    ...
  ]
}

We will need to also grab user data through an another API call. We will have two stores (userStore.js and commentStore.js), as it’s pretty likely another component will rely on that user API call and we don’t want to needlessly request the same data again.

So how do we accomplish this? We could initialize a fetch on both stores from the Comment component, but then it has to manage loading state from all components it called. It also feels wrong to have a component know all of a data store’s dependencies—that could get out of hand quickly. Instead, we moved this logic to actions and stores.

// CommentActions.js
fetchDataIfNecessary: function() {
  loadingState = DomainStore.data().loadingState;
  if (loadingState === 'initial' || loadingState === 'failed' ) {
    UserActions.fetchDataIfNecessary()
    some.ajax.library('api/comments/index', this.handleSuccess, this.handleFailure);
  }
}

// UserActions.js
fetchDataIfNecessary: function() {
  loadingState = DomainStore.data().loadingState;
  if (loadingState === 'initial' || loadingState === 'failed' ) {
    some.ajax.library('api/users/index', this.handleSuccess, this.handleFailure);
  }
}

// CommentStore.js
initialize: function() {
  this._data = { loadingState: 'initial', comments: [] }
  UserStore.addChangeListener(this.onUserStoreChange);
}

data: function() {
  // return this._data zipped with UserStore.data()
}

onUserStoreChange: function() {
  // Update loading state based on both my loading state and User loading state
  this.emitChange();
}

Actions still kick off API calls, but they also trigger an API call if necessary on any dependent stores. The Comments store will also only emit the 'loaded' state when all its dependencies are loaded. If any store is updated, the child store can respond and update itself as well. If there are more dependencies, simply boolean AND all of the loadingStates and return the result as the component’s exposed loadingState.

Ongoing Challenges

We are pretty happy with how quickly we were able to solve problems with React and Flux. I’m sure we will find other corner cases and difficulties in our Flux architecture, but I feel we have solved many of the front-end problems we are faced with.

There is still plenty of room for improvement! Here are a few things we are still thinking about:

Thanks for reading! Good luck with Flux!

Nathanael is a full-stack developer on the Studio Engineering Team at Kapost. He is passionate about improving front-end development with better React and UX patterns. Follow him on Twitter @NBeisiegel.

Comments 8
  • Csaba Okrona
    Posted on

    Csaba Okrona Csaba Okrona

    Reply Author

    Awesome article, it gives both theory and practical examples. Have you tried Flux alternatives e.g. Reflux?


    • Nathanael Beisiegel
      Posted on

      Nathanael Beisiegel Nathanael Beisiegel

      Reply Author

      Hey there Csaba! Glad you found the article helpful. I haven’t looked into Reflux in any depth, but I have spent some time with Flummox (http://acdlite.github.io/flummox). If we were to start fresh with a new app I would probably chose Flummox. It’s close to vanilla Flux but allows for isomorphic / universal rendering by avoiding singletons. It also removes action / constant boilerplate at the cost of being slightly more opinionated and having a parent ‘Flux’ object.


  • Nathanael Beisiegel
    Posted on

    Nathanael Beisiegel Nathanael Beisiegel

    Reply Author

    In case anyone is reading this a month later, we have improved how we manage store dependences by moving that logic back to the components. We came up with a slightly more sophisticated `connectToStores` higher-order component that kicks off a fetch if necessary to preload all data. See this fantastic article for ideas on starting with HoCs (https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750). Hopefully I’ll be able to write an article on that in the near future.