Yo in Flux

Written by David on September 13, 2014

Intro

Yo in React was my first React application and I didn’t know anything about Flux when I wrote it. In each of my React components, I was passing in props so I could figure out the Firebase URL to bind to using the ReactFireMixin and then called it good.

Admittedly, it was ugly and inflexible. All of the data was in Firebase, but it didn’t have a single centralized place in my client side code. I basically connected to whatever I needed and manipulated whatever I needed, and did this in 4 or 5 different components, and it was already starting to get hard to follow and reason about.

Once Flux was introduced into the codebase, things started to get a lot nicer, because the data was located in one centralized file (the “Store”) and components subscribed to it for changes. Actions were clear because they were invoked by components, and Stores explicitly handled these Actions. I ended up winning a lot of organization, maintainability, and conventional patterns by migrating to the Flux architecture.

Demo/Github

So I created a new repository and hosted a new demo (which is functionally the same as Yo-in-React) here:

Demo: https://yo-in-flux.firebaseapp.com

Github: https://github.com/davidchang/yo-in-flux

Flux Actions

The actions we’ll need are:

  • The User logged in to the app
  • The User logged out of the app
  • Yo a Person
  • Add a Person to Yo
  • The User was editing the Add Person Form (this doesn’t necessarily need to be handled by a store - I’m a little fuzzy as if it should or not)

Flux Stores

I ended up creating two stores. The YoStore (poorly named) handles user interaction and owns that user’s data (their Yo’s, people they can Yo) and can also sends Yo’s. The AddPersonStore is responsible for the add person functionality (the form input field, validation, and actually adding the person to the YoList).

The anatomy of a store, at least from Facebook’s philosophy, is that it should expose EventEmitter actions (like emitChange, addListener, removeListener) and it should expose functions to access private data. EventEmitter and data accessor methods are used by view components. Not only that, but the Store needs to emit a change to its listeners whenever any of its private data changes. If our backend is Firebase, we can easily just set up a Firebase listener and then emit a change. That looks like this:

authenticatedUserRef = firebaseConnection.child('/users/' + authenticatedUser);

// only listening on child_added is actually totally sufficient because these entities are only ever added
authenticatedUserRef.child('notifications').on('child_added', function(dataSnapshot) {
  notifications.push(dataSnapshot.val());
  emit(); //emit is a debounced function that calls this.emit from the EventEmitter prototype
});

The store also needs to register itself with the Dispatcher and respond to events that it cares about.

That registration/listening code looks just like this:

// Register to handle all updates
AppDispatcher.register(function(payload) {
  var action = payload.action;
  var text;

  switch(action.actionType) {
    case YoConstants.YO_SEND_YO:
      sendYo(action.person);
      break;

    case YoConstants.YO_USER_AUTHENTICATED:
      initializeUser(action.person);
      break;

    case YoConstants.YO_USER_UNAUTHENTICATED:
      authenticatedUser = '';
      authenticatedUserRef = '';
      break;

    default:
      return true;
  }

  return true; // No errors.  Needed by promise in Dispatcher.
});

Flux View-Controllers

View Controllers don’t need to change too much in terms of their View, but they do need to consume data differently. This is done by setting up listeners to the stores, in this very common code snippet:

componentDidMount: function() {
  YoStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
  YoStore.removeChangeListener(this._onChange);
},
_onChange: function() {
  this.setState(getState());
}

where getState is something like this:

var getState = function() {
  return {
    notifications: YoStore.getNotifications()
  };
};

And that’s it.

I ended up doing a bunch of refactoring that I should have been done originally. For instance, I had both the YoList and the form/logic to add to your YoList in a single component in the original project, but that really should have been separated or at least granularized.

Open Source Consumed

I’m using the React Webpack Yeoman generator and so used Webpack as my module bundler (for the first time ever), Facebook’s Flux that is now available via NPM, and react-router-component. I had some difficulty because I wanted to do hosting via Firebase or Github and only wanted static files, so I couldn’t route all traffic to the same index.html page (which would be easy in something like Express). Since I only had two paths in the entire app, I ended up copying index.html into authRequired.html, but anything more than that would have felt guilty.

I again used Firebase for storing my data, simple login via Twitter, and hosting. Firebase hosting is very impressive to me - still just as easy as firebase init && firebase deploy. Since the Yeoman generator would build and dump into the dist/ directory, I could actually just tell Firebase that my public directory was dist/ and that’s all it would use. Awesomely convenient.

Conclusions

My overall experience with Flux was that it had a pretty big learning curve for me personally, but things make a lot of sense and are easier to reason about thanks to the unidirectional flow. I still have some confusion as to what actually belongs in stores (related to this Google Groups exchange) and am not sure whether API calls should be made in the Action or the Store (it mattered less here, because Firebase emits change events anyways).

Overall experience with Firebase is still overwhelmingly positive. ReactFire itself is nice for little demos, but doesn’t lend itself to larger applications that require better code organization/modularization.

If you made it this far, many thanks for reading!