What the Flux?

javascript Jan 27, 2015

I've been really diving into wrapping my brain around React.js and Flux architecture the last couple of weeks.

Not going to lie, I've been avoiding it. When I look at the docs and blog posts about Flux, and React, my brain goes...

Things like componentDidMount, and Dispatchers, and ActionCreators, etc etc initially scared me off when I tried to get into it. Just seemed so computer sciency™. After a few days of messing with it, I really like it. A lot.

Flux in particular took me a few days to really wrap my head around. This image in particular.

When I finally started to wrap my head around it though, I decided to come up with a version of my own to help others get around to figuring it out a bit faster.

Let's break this all down...

Views

Show some stuff.

This is where your components render. Your component has only a few jobs, and here's what one might look like...

var NewEmailComponent = React.createClass({
	getInitialState() {
    	return {
        	to: ""
        };
    },
	render() {
      return (
              <input name="to" type="email" onChange={this.updateToText} />
              <button onClick={this.createNewEmail}>New Email</button>
          );
	},
    updateToText(event) {
		this.setState({
        	to: event.target.value 
        });
	},
	createNewEmail(event) {
		var to = this.state.to;
    	EmailActionCreator.createNewEmail(to);
	}
});

So, every component in React has state. Our NewEmailComponent will initially have just a to: "" in its state.

Next, render returns JSX to display. Here we have an input, and a button.

You'll notice the onChange={this.updateToText}, and the onClick={this.createNewEmail}. Both of these are adding event handlers to the component, and calling the respective functions.

When the text of the to input changes, the state gets changed to reflect that to has changed.

Action Creators

Go get some more stuff

The action creator creates an "action". Yup. It is also here that you'll typically go get some data from the server if you need it.
So, let's go back to the view for a sec...

When you click on the button, the EmailActionCreator.createNewEmail(to) function fires with whatever kind of data you want to pass as to. Could be an ID, name, whatever, in this case it's an email to send to.

The EmailActionCreator.createNewEmail function will do a couple of things.

  1. Create an action for creating a new email
  2. Call an API to create a draft

Actions

Do something with stuff

Actions are JavaScript POJOs (plain old javascript objects). Often they will by convention have a type, and some data. Let's step back to how and why they are created in the first place.

So, our ActionCreator will create an "action". Typically there is a file that stores an object of all the different possible action types.

// actions/fooActions.js
var keyMirror = require('keymirror');

module.exports = keyMirror({
	CREATE_EMAIL: null,
    EMAIL_CREATED: null,
    SEND_EMAIL: null
});

You use these actions to define things you can trigger, whether from the UI or from the server.

keyMirror here is used to make the values of null, mirror the upper case key names. Just a quick short cut library.

So, back in our EmailActionCreator, we'll use these different action types.

// actions/emailActionCreator.js

module.exports = {
	createNewEmail(to) {
        EmailAppDispatcher.handleViewAction({
        	type: ActionTypes.CREATE_EMAIL,
            to: to
        });
        
        EmailWebService.createEmail({
          to: to
        });
    },
    emailCreated(mail) {
    	EmailAppDispatcher.handleServerAction({
        	type: ActionTypes.EMAIL_CREATED,
            mail: mail
        });
    }
}

The EmailWebService can hit your API however you want...

// data/emailWebService.js
module.exports = {
	createEmail: function(options) {
		$.ajax({
        	url: "/api/email",
            type: "POST"
        }).done(function(id) {
            var mail = { 
            	to: options.to, 
                id: id 
            };
            
        	EmailActionCreator.emailCreated(mail);
        });
	};
};

Here, it's using jQuery.ajax to create a new email. Once the email is done being created, another action is created with EmailActionCreator.emailCreated(mail); to tell everyone that the email has been created. The type of this action is ActionTypes.EMAIL_CREATED.

Now let's talk about the dispatcher and that handleViewAction and handleServerAction stuff.

Dispatcher

Hey Everyone! There's new stuff!

The dispatcher's single job is to publish out some sort of message, or event, or whatever you want to call it, to let anybody who cares know that something just happened.

For example, when someone first clicked our Create Email button, an action was created with the EmailActionCreator, and then sent over to the dispatcher via the handleViewAction method on the dispatcher.

The dispatcher is boilerplate code that never really needs to change once it's done...

var EmailAppDispatcher = assign(new Dispatcher(), {
  handleServerAction(action) {
    var payload = {
      source: PayloadSources.SERVER_ACTION,
      action: action
    };
    this.dispatch(payload);
  },
  handleViewAction(action) {
    var payload = {
      source: PayloadSources.VIEW_ACTION,
      action: action
    };
    this.dispatch(payload);
  }
});

module.exports = EmailAppDispatcher;

Flux.js comes with a default dispatcher that has register and dispatch functions among a few others.

Our EmailAppDispatcher has 2 helper functions, the handleViewAction, and handleServerAction. Two different functions really aren't entirely neccessary, but they allow you to know exactly where an action came from if you need it. Beyond that though, they just call the built in dispatch function from the default Dispatcher in Flux.

Elsewhere in the app, any store, which we'll talk about in a second, that uses the Dispatcher's register function will be notified that an action is being dispatched.

Store

Keep track of stuff

Stores hold application state and business logic. They are also themselves usually event emitters. That means you can listen to events they publish. So, a view will bind to a store's change event, call different store methods to get data, and in turn update their state with the new data.

Ideally, stores will not make any ajax requests because the actions should send in data, and the store just manages it.

A store will generally have local variables that hold some kind of data/state. So, in our case below, simply the active email.

var EventEmitter = require("events"),
	assign = require("react/lib/assign"),
    Email = require("./core/email"),
    _ = require("underscore");

var CHANGE_EVENT = "change";

var activeEmail = null;

function createNewEmail(mail) {
	var email = new Email(mail);
    
    activeEmail = email;
}

var EmailStore = assign({}, EventEmitter.prototype, {
  emitChange() {
  	this.emit(CHANGE_EVENT);
  },
  addChangeListener(callback) {
  	this.on(CHANGE_EVENT, callback);
  },
  removeChangeListener(callback) {
  	this.removeListener(CHANGE_EVENT, callback);
  },
  getActiveEmail() {
  	return activeEmail;
  }
});

The emitChange, addChangeListener, and removeChangeListener are functions you'll see on every store, and are just abstractions to the underlying EventEmitter methods emit, on, and removeListener. The CHANGE_EVENT just saves a string for the name of the change event.

The getActiveEmail method just returns the active email.

Backing up again to the dispatcher, remember it exposes a register function. After we define the methods for our store as above, we register with our dispatcher...

EmailStore.dispatchToken = EmailAppDispatcher.register((payload) => {
	var action = payload.action;
    
	switch (action.type) {
    	case ActionTypes.EMAIL_CREATED:
        	createNewEmail(action.mail);
            EmailStore.emitChange();
            break;
        	
    }
});

Every time that the dispatcher sends out ANY action, the register function gets called. However, we only care about the action if the type is in our switch statement, and in this case all we have is ActionTypes.EMAIL_CREATED.

When the action sent out by the dispatcher is ActionTypes.EMAIL_CREATED, we'll respond by calling our createNewEmail function which merely sets an active email. Then we'll tell the store to emit a change event.

Going full circle, let's step back and add something to our view...

var EmailStore = require("../stores/emailStore");

/** ... *./
{
	componentWillMount() {
		EmailStore.addChangeListener(this._onChange);
	},
    _onChange() {
    	this.setState({
        	activeEmail: EmailStore.getActiveEmail()
        });
    }
}

Our componentWillMount function will fire when the component is ready. It's in componentWillMount that we can add a change listener on our store's change event. Any time that the store changes, this._onChange gets called.

In the _onChange function we can set the state of the component with the new email that was created!

Boom.

TL;DR

View Renders ->
Click triggers action creator ->
Go get data ->
Create action for recieved data ->
Send action to dispatcher ->
Dispacther publishes action ->
Store listens for action ->
Store updates state ->
View updates

So, now you're either thinking...

Or...

If you're wanting to flip a table, re-read this article, go look at these exampes, and try again. You'll get it, and you'll love it.

Tags