Saturday, February 28, 2015

Overview

Flux is a type of web application architecture that plays nicely with React’s unidirectional data flow. You can find all the information from this post and a lot more in the Full Stack Flux video and the HTMLDevConf React Flux video. The images are from both those videos, the creators own all the rights to them.



Old Way

Let’s take the facebook chat as the example. If we wanted to get the number of unread messages for each thread (unread messages per person).

Chat Notification

1
2
3
4
5
6
7
var unreadMessageCounts = {};
function onNewMessage(msg){
    // increment the old count and cache the new count
    var unreadCount = ++unreadMessageCounts[msg.threadId];
    // update DOM
    $('#messageCount' + msg.threadId).text(unreadCount);
}

This is nice and all, but what happens if we want to know how many threads have unread messages (number of conversations that have unread messages)? We would have to update our code.

Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var unreadMessageCounts = {};
var unreadThreadCount = 0;

function onNewMessage(msg){
    // increment the old count and cache the new count
    var unreadCount = ++unreadMessageCounts[msg.threadId];
    if (unreadCount === 0) {
        // increment number of unread threads
        ++unreadThreadCount;
        // update DOM for threads
        $('#unreadThreads').text(unreadThreadCount);
    }

    // update DOM for the conversation
    $('#messageCount' + msg.threadId).text(unreadCount);
}

You then create models that hold the information. Like a Conversation model and a Notification model. Well the Notification model needs to know when the Conversation model updates like when someone sends you a message and a little unread icon appears next to messages.

It’s complicated already… It becomes even more complicated with more models.


Some people use pub/sub which is nice… until you want the models to get the data in a certain order. (The notification bar which holds everything gets the message before the Conversation).



New Way

There is a clean way to handle this issue. Using a unidirectional data flow. Flux’s architecture looks something like this.

Notification


Action: object with a type property and new data

Action Creator: methods that create Actions, they become the API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ### in FooActionCreator.js ###

var AppDispatcher = require('../AppDispatcher');
var AppConstants = require('../AppConstants');

var ActionTypes = AppConstants.ActionTypes;

module.exports = {
    // Action Creator
    createMessage(text){
        // That new object is an Action
        AppDispatcher.dispatch({
            type: ActionTypes.MESSAGE_CREATE,
            text: text
        });
    }
};


Dispatcher: It’s basically a registry of callbacks. The Flux dispatcher is a singleton. Payload is an Action. Primary API: dispatch(), register(), waitFor()

1
2
3
4
5
6
// ### in AppDispatcher.js ###

var Dispatcher = require('Flux.Dispatcher');

// export singleton
module.exports = new Dispatcher();


Store: Each store is a singleton. Holds the data. Only way into the store is through the callback from the Dispatcher. Only has getters, no setters. It emits “I changed” events when the state changes.

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
32
33
34
35
36
37
38
39
// ### in FooStore ###

var _dispatchToken;
var _messages = {};

class FooStore extends EventEmitter {
    constructor(){
        super();

        _dispatcherToken = AppDispatcher.register( (action) => {
            switch(action.type){
                case ActionTypes.MESSAGE_CREATE:
                    var message = {
                        id: Date.now(),
                        text: action.text
                    };
                    _messages[message.id] = message;
                    this.emit('change');
                    break;

                case ActionTypes.MESSAGE_DELETE:
                    delete _messages[action.messageId];
                    this.emit('change');
                    break;

                default:
            }// End switch
        });// End AppDispatcher.register
    }// End constructor

    getDispatchToken(){
        return _dispatchToken;
    }

    getMessages(){
        return _messages;
    }
}
module.exports = new FooStore();


Views: Tree of react components. The components near the top of the tree listen for store changes. When there’s a store change, they query the store for new data and pass it down to children.

Lifecycles


We fist create our ControllerView which retrieves the data from the Store.

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 FooControllerView.react.js ###

var React = require('react');

var FooControllerView = React.createClass({
    getInitialState: function(){
        return {
            messages: FooStore.getMessages();
        };
    },

    componentDidMount: function(){
        FooStore.on('change', this._onChange);
    },

    componentWillUnmount: function(){
        FooStore.removeListener('change', this._onChange);
    },

    _onChange: function(){
        this.setState({
            messages: FooStore.getMessages();
        });
    },

    render: function(){
        //TODO
    }
});

module.exports = FooControllerView;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
render: function(){
    var messageListItems = this.state.messages.map(message => {
        return (
            <FooChild
                key={message.id}
                id={message.id}
                text={message.text}
            />
        );
    }); // End messageListItems

    return (
        <ul>
            {messageListItems}
        </ul>
    );
}// End render


We now have to create FooChild which will display each message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ### in FooChild.react.js ###

var React = require('react');

var FooChild = React.createClass({
    propTypes = {
        id: React.PropTypes.number.isRequired,
        text: React.PropTypes.number.isRequired
    }

    render: function(){
        return (
            <li>{this.props.text}</li>
        );
    }
});

module.exports = FooChild;



Initialization

Inside AppBoostrap.js, we use the dispatcher and have it load the contents of every store and only after every store has content do we render.



Web Api

Put all the Api calls into a WebApiUtils module.


Random Posts