How I Learned to Stop Worrying And Love The Flux

j@ideadevice.com

@jaseemabid

Hello!

Demo!

Why not MVC?

 

  • This talk is about how we did it.
  • Take away what you want, innovate.

 

Opinionated

  • Messages that go through the system
  • Type and Data

Actions

{
   "type": "NEW_MESSAGE",
   "data": {
       "id": 42,
       "message": "Hello Meta Refresh"
   }
}

Dispatcher

  • Central broker that sends actions to registered stores

  • We went with Facebook's own implementation 

Another pub sub?

  • Callbacks are not subscribed to particular events

  • Every payload is dispatched to every registered callback

  • Callbacks can be deferred

...
Dispatcher.register(function(payload) {
    if (payload.type === 'NEW_MESSAGE') {
        console.log(payload.message);
    }
});
...

...
// On some events: Web Sockets, API Responses, etc.
Dispatcher.dispatch({
    type: 'NEW_MESSAGE',
    message: 'Hello Meta Refresh'
});
...

Stores

  • Thin wrapper around Immutable.js
  • Holds data as well as state
  • Stateful, but not Asynchronous
  • Listens to Actions
  • Emits changes from action payloads
  • ItemStore and IndexStore
class Store {
    constructor(spec, dispatchToken) {
        this.dispatchToken = dispatchToken;
        assign(this, EventEmitter.prototype, spec);
        this.init();
    }

    emitChange() {
        this.emit('change');
    }

    addChangeListener(callback) {
        this.on('change', callback);
    }

    removeChangeListener(callback) {
        this.removeListener('change', callback);
    }
}
var UserStore = new Store({
    getAll: function() {
        if (Object.keys(this._items).length === 0) {
            this._items = {
                state: 'PENDING'
            };
            UserActions.get();
        }
        return this._items;
    },

    getByID: function(id) {
        if (!this._items[id]) {
            this._items[id] = {
                state: 'PENDING'
            };
            UserActions.get(id);
        }
        return this._items[id];
    }

}, function () { ... });
var UserStore = new Store({
        ...
}, Dispatcher.register(payload) {
    var type = payload.type,
        users = payload.users;

    switch(type) {
        case 'USER_GET_SUCCESS':
            users.forEach(function(user) {
                this._items[user.id] = user;
            }.bind(this));
            this.emitChange();
            break;

        case 'USER_GET_FAILURE':
            this._items = {
                state: 'FAILURE'
            };
            this.emitChange();
            break;
    };
});
UserActions = {
    get: function(id) {
        Dispatcher.dispatch({
            type: 'USER_GET_PENDING'
        });

        // API Wrapper
        getUsers(id, function(err, users) {
            if (err) {
                Dispatcher.dispatch({
                    type: 'USER_GET_FAILURE',
                    error: err
                });
            } else {
                Dispatcher.dispatch({
                    type: 'USER_GET_SUCCESS',
                    users: users
                });
            }
        });
    }
};

A lot more on stores

  • Not just models and collections

  • Listens and act on actions

  • Stores don't mutate themselves

  • Singletons

  • May talk to other stores, but we avoided it

  • React 
  • Almost stateless
  • Synchronous. No XHR, no promises, no callbacks
  • Views get data from stores and listen for changes
  • render :: State → HTML

Views

var UserList = React.createClass({

    componentWillMount: function() {},
    
    componentDidUpdate: function() {},
    
    getInitialState: function() {},
    
    getStateFromStores: function() {},

    setStateFromStores: function() {},

    render: function() {}
}); 
...

getInitialState: function() {
    return {
        users: this.getStateFromStores()
    };
},

getStateFromStores: function() {
    return UserListStore.getAll();
},

setStateFromStores: function() {
    this.setState({
        users: this.getStateFromStores()
    });
}

...
...

componentDidMount: function() {
    UserIndexStore.addChangeListener(
        this.setStateFromStores);
},

componentWillUnmount: function() {
    UserIndexStore.removeChangeListener(
        this.setStateFromStores);
}

...
...

render: function() {
    var users = this.state.users;

    return (
        <table>
            {users.map(function(user) {
                return (
                    <tr>
                        <td>{user.name}</td>
                        <td>{user.email}</td>
                     </tr>
                );
            })}
        </table>
    );
}

...
!

.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

this.state & this.props

var UserList = React.createClass({

    ...

    onSubmit: function(e) {
        e.preventDefault();
        if (this.state.isNew) {
            UserActions.create(
                this.state.user.toJSON());
        } else {
            UserActions.update(
                this.state.user.toJSON());
        }
    }

    ...

});
UserActions = {
    ...
    create: function(user) {
        Dispatcher.dispatch({
            type: 'USER_CREATE',
            message: 'Creating User "' + user.username + '"',
            item: user
        });

        // The async bit
        createUser(user).then(function(data) {
            Dispatcher.dispatch({
                type: 'USER_CREATE_SUCCESS',
                item: user,
                data: data
            });

        }, function(err) {
            Dispatcher.dispatch({
                type: 'USER_CREATE_FAILURE',
                item: user,
                error: err
            });
        });
    }
}

Dependent stores

  • Data dependency between stores

  • IndexStore `waitFor` ItemStore

class IndexStore extends Store {
    constructor(ItemStore, spec, dispatcherCallback) {

        super(spec, Dispatcher.register(function(payload) {

            if (payload.action.type === this.GET_SUCCESS_ACTION) {

                // Wait for the related item stores to finish 
                Dispatcher.waitFor([ItemStore.dispatchToken]);
            ...

Testing

  • React.addons.TestUtils
  • Mocha vs Jest
describe('User list testing', () => {

    it('Renders a loading page', () => {
        // Render the Expiry tab into the document
        var userlist = TestUtils.renderIntoDocument('<UserList/>'),
            node = expiry.getDOMNode();

        expect(node.textContent).to.be("LOADING");
    });

    it('Shows atlest one user', () => {

        // Fire user get action
        Dispatcher.fire({
            type: 'USER_GET_SUCCESS',
            data: [{
                name: 'John Doe'
            }]
        });

        expect(node.textContent).to.be(...);
    }):

});

Some lessons

  • Fewer network requests in certain cases
  • Testable to a large extend
  • Asynchronous code localized to action creators
  • State localized to stores mostly
  • Views mostly stateless, synchronous 
  • Playbacks with actions
  • Detailed analytics

 

There is a lot more...

  • Optimistic UI updates
  • Excellent caching
  • Immutable.js backed actions and stores
  • React.js views
  • hasEnoughDetail
  • Error handling 
  • Awesome routing

flux

By jaseemabid

flux

  • 1,016