Building an ES6/JSX/React Flux App – Part 2 – The Flux

In the first part of this tutorial, I concentrated on React views. In fact, I didn’t do anything else. Except for a little bit of in-component interactivity, everything that resulted from those views was static DOM. I could have written a little bit of JavaScript inside the HTML page to get the same effect. Smaller applications like these seem to make the frameworks cumbersome, but they demonstrate core features in an isolated setting. React and Flux (and pretty much any other architecture, framework or library) really come into their own in larger applications.

The same is true of Flux. Flux is the architecture that goes along with React. It does the same job as MVC – an interactive page – but in a very different way. Flux is definitely not MVC, as you can see from the architecture diagram:

08192015-1

In a Flux architecture, the Dispatcher is a central co-ordination point. It receives actions and dispatches them to stores. Stores react to those actions, adjusting their data, before informing any dependent views of a change. The actions can come from anywhere – external events like AJAX loads as well as user events like clicks or changes.

Normally, I would go to a library to implement such an architecture. If I were in an MVC world, I would be getting AngularJS or EmberJS to assist. There are lots of libraries out there (Fluxxor, Reflux and Alt to name a few). They all implement the full Flux architecture, have great tutorials in written and video form and are full featured. However, I found them difficult to wrap my head around. That’s mostly because I was wrapping my head around Flux – the design pattern – and their library at the same time. I’m more interested in the design pattern.

So I wrote my own. I’m calling this “Flux Light”. It doesn’t implement some of the more cumbersome features of Flux. It’s written entirely in ES6 and it’s opinionated. It expects you to write your code in ES6 as well. It also expects no overlapping stores (i.e. one store does not depend on another). It expects to be bundled with browserify or similar. I make no apologies for this. It’s a learning exercise and nothing more. (Of course, if folks feel that it’s useful to them, let me know and I will publish it on npm). The code for the library is located in Client/lib in the repository. I’m going to show how to use it.

The Dispatcher

The Dispatcher is the center of the Flux architecture. It has a couple of requirements:

  • Components should be able to use dispatch() with an Action
  • Stores should be able to register for actions

All actions flow through the Dispatcher, so it’s code surface should be small and simple. This code is located in lib/Dispatcher.js. That is only the class though. You will want to initialize the dispatcher. Do this in Client/dispatcher.js like this:

import Dispatcher from './lib/Dispatcher';

var dispatcher = new Dispatcher({
  logLevel: 'ALL'
});

export default dispatcher;

The only options available are logging ones right now. I use the logLevel to set the minimal log level you want to see in the JavaScript console. The most common log levels will be ‘ALL’ or ‘OFF’. If you don’t specify anything, you will get errors only.

Actions

Actions are just calls into the dispatcher dispatch() method. I’ve got a class full of static methods to implement actions, located in Client/actions.js, like so:

import dispatcher from './dispatcher';

export default class Actions {
    static navigate(newRoute) {
        dispatcher.dispatch('NAVIGATE', { location: newRoute });
    }
}

I can use this class to generate actions. For example, I want to have the NAVIGATE action happen when I click on one of the navigation links. I can adjust the NavLinks.jsx file like this:

import React from 'react';
import Actions from '../actions';

class NavLinks extends React.Component {
    onClick(route) {
        Actions.navigate(route);
    }

    render() {
        let visibleLinks = this.props.pages.filter(page => {
            return (page.nav === true && page.auth === false);
        });
        let linkComponents = visibleLinks.map(page => {
            let cssClass = (page.name === this.props.route) ? 'link active' : 'link';
            let handler = event => { return this.onClick(page.name, event); };

            return (<li className={cssClass} key={page.name} onClick={handler}>{page.title}</li>);
            });

        return (
            <div className="_navlinks">
                <ul>{linkComponents}</ul>
            </div>
        );
    }
}

Here I tie the onClick method via an event handler to the click event on the link. That, in turn, issues a navigate action to the dispatcher via the Actions class.

Somewhere, I have to bootstrap the dispatcher. That’s done in the app.jsx file:

import React from 'react';
import dispatcher from './dispatcher';
import AppView from './views/AppView.jsx';

dispatcher.dispatch('APPINIT');

React.render(<AppView/>, document.getElementById('root'));

The AppStore

In my prior example, I was passing the pages and route down from the bootstrap app.jsx file. I’m now going to store that information in a Store – a flux repository for data. I’ve got a nice API for this. Firstly, there is a class you can extend for most of the functionality – lib/Store. You have to implement a constructor to set the initial state of the store and an onAction() method to handle incoming actions from the dispatcher.

The API for the Store contains the following:

  • Store.initialize(key,value) initializes a key-value pair in the store.
  • Store.set(key, value, [squashEvents=false]) sets a key-value pair in the store. Normally, a store changed event will be triggered. If you set squashEvents=true, then the store changed event is squashed, allowing you to set multiple things before issuing a store changed event.
  • Store.get(key) returns the value of the key in the store.
  • Store.storeChanged() issues a store changed event.
  • Store.registerView(callback) calls the callback whenever the store is changed. Returns an ID that you can use to de-register the view.
  • Store.deregisterView(id) de-registers a prior view registration.

Creating a store is simple now. You don’t have to worry about a lot of the details. For instance, here is my store/AppStore.jsx file:

import Store from '../lib/Store';
import find from 'lodash/collection/find';
import dispatcher from '../dispatcher';

class AppStore extends Store {

    constructor() {
        super('AppStore');
        this.logger.debug('Initializing AppStore');

        this.initialize('pages', [
          { name: 'welcome', title: 'Welcome', nav: true, auth: false, default: true },
          { name: 'flickr', title: 'Flickr', nav: true, auth: false },
          { name: 'spells', title: 'Spells', nav: true, auth: true }
        ]);
        this.initialize('route', this.getNavigationRoute(window.location.hash.substr(1)));
    }

    onAction(actionType, data) {
        this.logger.debug(`Received Action ${actionType} with data`, data);
        switch (actionType) {

            case 'NAVIGATE':
                let newRoute = this.getNavigationRoute(data.location);
                if (newRoute !== this.get('route')) {
                    this.set('route', newRoute);
                    window.location.hash = `#${newRoute}`;
                }
                break;

            default:
                this.logger.debug('Unknown actionType for this store - ignoring');
                break;
        }
    }

    getNavigationRoute(route) {
        let newRoute = find(this.get('pages'), path => { return path.name === route.toLowerCase(); });
        if (!newRoute) {
            newRoute = find(this.get('pages'), path => { return path.default && path.default === true; });
        }
        return newRoute.name || '';
    }
}

var appStore = new AppStore();
dispatcher.registerStore(appStore);

export default appStore;

The constructor uses initialize() to initialize two data blocks – the pages and the route from the original app.jsx. The onAction() method listens for NAVIGATE actions and acts accordingly.

At the end, I create a singleton version of the AppStore and register it with the dispatcher – this causes the dispatcher to send actions to this store.

The Controller-View

In the Flux architecture, there are two types of React components. Controller-Views are linked to one or more stores and use state to maintain and update their children. Other React components are not linked to stores and DO NOT USE STATE – they only use props.

Controller-Views need to do the following:

  • Call registerView on each store they are associated with when the component is mounted (use the componentWillMount() lifecycle method)
  • Call deregisterView on each store they are associated with when the component is about to be unmounted (use the componentWillUnmount() lifecycle method)

In my previous post, I created an AppView.jsx to use state with the start of a router. I can alter that to become a Controller-View:

import React from 'react';
import appStore from '../stores/AppStore';
import NavBar from '../views/NavBar';
import Welcome from '../views/Welcome';
import Flickr from '../views/Flickr';
import Spells from '../views/Spells';

class AppView extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            pages: [],
            route: 'welcome'
        };
    }

    componentWillMount() {
        this.appStoreId = appStore.registerView(() => { this.updateState(); });
        this.updateState();
    }

    componentWillUnmount() {
        appStore.deregisterView(this.appStoreId);
    }

    updateState() {
        this.setState({
            route: appStore.get('route'),
            pages: appStore.get('pages')
        });
    }

    render() {
        let Route;
        switch (this.state.route) {
            case 'welcome': Route = Welcome; break;
            case 'flickr': Route = Flickr; break;
            case 'spells': Route = Spells; break;
            default: Route = Welcome;
        }

        return (
            <div id="pagehost">
                <NavBar pages={this.state.pages} route={this.state.route}/>
                <Route/>
            </div>
        );
    }
}

export default AppView;

The constructor just sets some empty state variables. When the component is mounted, the view is registered with the store and the state is updated from the store. When the component is unmounted (or just before), the view is deregistered again. The function updateState() is the callback that the store uses to inform the view of an updated state.

In the render() function, I’ve expanded the number of routes to the full complement. This also matches the list defined in the AppStore. If you wanted to keep the definition of the pages in one place, you could issue an action from this component to set the pages, then let the state update them once they’ve gone through the system. You would not want to just set the pages within the store. That would most definitely not be flux-like.

To round out this sequence, I’ve added a couple of pages – Flickr.jsx and Spells.jsx – as simple static React components. You can get this code drop from my GitHub Repository.

Handling AJAX in a Flux Architecture

Let’s say I wanted to fill in the details of the Flickr page. This is designed to bring the first 20 images for a specific tag back to the page. To do that, I need to make an AJAX JSONP request to the Flickr API.

In a React world, I will do two actions. The first will be when the Flickr page comes into focus. I want to issue a “REQUEST-FLICKR-DATA” action at that point. This will cause the store to kick off the AJAX request. When the request comes back, the store will issue a “PROCESS-FLICKR-DATA” action with the data that came back. This ensures that all stores get notified of the AJAX request and response.

Why the store? The store is the central source of truth for all data within a Flux architecture. The views should not be requesting the data.

Why the second action? Well, in this simple application, we could use just one action – the request. However, let’s say you had another store that counted the number of in-flight AJAX requests – or one that did nothing but do AJAX requests (for example, to allow the inclusion of authentication tokens). You may have one store handling the request and one store handling the response.

To implement this, I first added the actions to my actions.js file:

import dispatcher from './dispatcher';

export default class Actions {
    static navigate(newRoute) {
        dispatcher.dispatch('NAVIGATE', { location: newRoute });
    }

    static requestFlickrData(tag) {
        dispatcher.dispatch('REQUEST-FLICKR-DATA', { tag: tag });
    }

    static processFlickrData(data) {
        dispatcher.dispatch('PROCESS-FLICKR-DATA', data);
    }
}

Then I altered the onAction() method within the stores/AppStore.js file:

            case 'REQUEST-FLICKR-DATA':
                let lastRequest = this.get('lastFlickrRequest');
                let currentTime = Date.now;
                let fiveMinutes = 5 * 60 * 1000;
                if ((currentTime - lastRequest) > fiveMinutes) {
                    return;
                }
                $.ajax({
                    url: 'http://api.flickr.com/services/feeds/photos_public.gne',
                    data: { tags: data.tag, tagmode: 'any', format: 'json' },
                    dataType: 'jsonp',
                    jsonp: 'jsoncallback'
                }).done(response => {
                    Actions.processFlickrData(response);
                });
                break;

            case 'PROCESS-FLICKR-DATA':
                this.set('images', data.items);
                break;

In this case, I only request the Flickr data if five minutes have elapsed. When the response comes back, I trigger another action. This is, in my case, processed just below the request by the PROCESS-FLICKR-DATA block. If five minutes have not elapsed, then no request is made and no changes to the page are made. You can flick back and forth between the welcome and flickr page all you want – it won’t change.

Of course, there was some setup required for this code to work:

import Store from '../lib/Store';
import find from 'lodash/collection/find';
import dispatcher from '../dispatcher';
import Actions from '../actions';
import $ from 'jquery';

class AppStore extends Store {

    constructor() {
        super('AppStore');
        this.logger.debug('Initializing AppStore');

        this.initialize('pages', [
          { name: 'welcome', title: 'Welcome', nav: true, auth: false, default: true },
          { name: 'flickr', title: 'Flickr', nav: true, auth: false },
          { name: 'spells', title: 'Spells', nav: true, auth: true }
        ]);
        this.initialize('route', this.getNavigationRoute(window.location.hash.substr(1)));
        this.initialize('images', []);
        this.initialize('lastFlickrRequest', 0);
    }

Don’t forget to add jquery to the list of dependencies in package.json or via npm install --save jquery.

Now that I have the actions and store sorted for the new data source, I can convert Flickr.jsx to a Controller-View and render the images that get loaded. I’m going to kick off the request in the constructor. Since I have a rudimentary cache going, it won’t hit the Flickr API badly. Here is the code in views/Flickr.jsx:

import React from 'react';
import Actions from '../actions';
import appStore from '../stores/AppStore';

class Flickr extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            images: [],
            tag: 'seattle'
        };

        Actions.requestFlickrData(this.state.tag);
    }

    componentWillMount() {
        this.appStoreId = appStore.registerView(() => { this.updateState(); });
        this.updateState();
    }

    componentWillUnmount() {
        appStore.deregisterView(this.appStoreId);
    }

    updateState() {
        this.setState({
            images: appStore.get('images')
        });
    }

    render() {
        let images = this.state.images.map(image => {
            let s = image.media.m.split('/');
            let fn = s[s.length - 1].split('.')[0];
            return (
                <div className="col-sm-6 col-md-3" key={fn}>
                    <a className="thumbnail"><img src={image.media.m}/></a>
                </div>
            );
        });

        return (
            <section id="flickr">
                <h2>Flickr</h2>
                <div className="row">{images}</div>
            </section>
        );
    }
}

export default Flickr;

There are a couple of things I could have done differently here. Firstly, I could have requested the flickr data only when the component was mounted. This version asks for the data when the component is created. I wanted to minimize those round trips to the Flickr API during testing and this seemed a reasonable way of doing it. Given that I have the cache functionality in the store, I could reasonably see moving the request to the componentWillMount() method.

I could have made the Flickr component just a regular component. This would have required me to change AppView so that it made the request and passed that down when the page was instantiated. I didn’t think this was a good idea. As the application expanded, I would have all sorts of extra code in the AppView. Making each “page” have state and be connected to a store seems much more reasonable to me.

Finally, I could have made the Flickr data its own store. In this application expands, you naturally want to have stores handle specific data. For instance, you might have a store that deals with “books” or “friends” or “images”. Inevitably, you will have a simple store that deals with navigation and authentication. So, yes, I see merit in creating another store called ImageStore in a larger application. It seemed overkill for this application.

You can get the code from my GitHub Repository.

Wrap Up

This is the end of the React/Flux version of the Aurelia tutorial, but not the end of my tutorial. I like to add authentication and monitoring to my applications as well, so I’ll be taking a look at those next.

This is a good time to compare for the four frameworks I have worked with. I’ve worked with Angular, Aurelia, Polymer and React/Flux now. Let’s discuss each one.

I felt Angular was heavy on the conventional use. It didn’t allow me to be free with my JavaScript. I felt that there was the Angular way and everything else was really a bad idea. Yes, it did everything I wanted, but the forklift upgrade (coming for v2.0) and the difficulty in doing basic things, not to mention the catalog of directives one must know for simple pages, meant it was heavier than I wanted to use.

Aurelia was a breath of fresh air in that respect. It worked with ES6, not against it. Everything is a class. However, the lack of data management right now, plus a relatively heavy-weight library on the download makes this a poor choice for simple applications. I suppose I could have done the same as I did here and used browserify to bundle everything together, but it isn’t easy.

Polymer is a third of a framework, in much the same way that React is a partial framework. It can’t stand alone. You need page routing and data management. These are, in the Polymer world, extra web components that you just download. Since there is no “import” system that allows for importing from standardized locations, you end up with a lot of hacking. Polymer will find a place, particularly when you consider the upcoming ES6-compatible MVC architectures like Angular-2 and Aurelia.

React/Flux is definitely focussed on large applications. Even my modest tutorial application (and not including the library code I wrote) was a significant amount of code. However, I can appreciate the flow of data and I understand that flow of data. That allows me to short-circuit the debugging process and go straight to the source of the problem. As the application grows, the heaviness of the framework coding becomes less of an issue.

I can see areas for improvement in the React/Flux code I wrote – some boilerplate that can be kicked out into classes all of their own. I like what I have here.

Mobile is another area that I am increasingly becoming interested in. With React, there is React Native – a method of using React code within an iOS application that compiles to a native iOS app. React is also much more usable in an Apache Cordova app – allowing Android and Windows Phone coverage as well. Angular works well with Apache Cordova (see the Ionic Framework). Aurelia does not work well with mobile apps yet. Polymer is “just another component”, but the polyfills that are necessary to make Polymer work on Apache Cordova are heavier than I expected.

Overall, I’ll continue watching React with great interest.

In my next post, I’ll cover authentication with my favorite authentication service – Auth0.