Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Isomorphic Flux

Michael Ridgway
November 06, 2014
220k

Isomorphic Flux

Talk for ReactJS SF Meetup

Michael Ridgway

November 06, 2014
Tweet

Transcript

  1. Isomorphic Flux
    Michael Ridgway
    @TheRidgway
    [email protected]

    View Slide

  2. React and Flux patterns can be used to build
    the holy grail of isomorphic applications.

    View Slide

  3. Isomorphic
    The ability to perform similar functionality in multiple
    environments

    View Slide

  4. Agenda
    ▪ Why Isomorphic?
    ▪ Isomorphic Spectrum
    ▪ Achieving the Holy Grail
    ▪ Flux Chat
    ▪ Problems to Address

    View Slide

  5. Why Isomorphic?

    View Slide

  6. Server Rendering
    ▪ Search engine optimization
    ▪ Legacy browser support
    ▪ User perceived performance

    View Slide

  7. ▪ Rich client interactions
    ▪ Faster page navigation
    ▪ No re-downloading assets every page
    ▪ No re-fetching data every page
    ▪ Just load the new data and new assets if needed
    Single Page Apps

    View Slide

  8. Why not both?
    All of the benefits of each AND we don’t have to
    duplicate code!

    View Slide

  9. First Principle of Rich Web Apps
    “server-rendered apps vs single-page apps: If we want to optimize for
    the best possible user experience and performance, giving up one or
    the other is never a good idea.”
    - Guillermo Rauch
    http://rauchg.com/2014/7-principles-of-rich-web-applications/

    View Slide

  10. Isomorphic Spectrum

    View Slide

  11. Fully Independent
    Client Server
    Model
    View
    Controller Model
    View
    Controller
    Routing Routing

    View Slide

  12. Converging
    Client Server
    Model
    Controller
    Model
    View
    Routing Routing
    Controller

    View Slide

  13. Client
    Controller Model
    View
    Routing
    Server

    View Slide

  14. Mojito was Yahoo’s first take on isomorphic MVC
    ● Used YUI on the server and the client
    Lessons learned:
    ● Browser abstractions are too slow for the server
    ● Synchronizing server, client, and DOM was hard
    ● Using YUI’s module format was problematic in node.js

    View Slide

  15. Achieving the Holy Grail

    View Slide

  16. Low Hanging Fruit
    ▪ Use CommonJS or ES6 instead of mixing formats
    ▪ Use polyfills instead of abstractions where possible
    ▪ es5-shim, intl.js, etc.
    ▪ Use libraries that are shareable
    ▪ lodash, superagent, moment, etc.
    ▪ Use bundling for building your javascript
    ▪ e.g. webpack or browserify

    View Slide

  17. React
    ▪ Works on the server and the client with one simple API
    change:
    ▪ Client: React.render(Component(), domElement, callback);
    ▪ Server: var html = React.renderToString(Component());
    ▪ Clean programmatic page composition using
    components
    ▪ DOM synchronization is easy, now only worry about
    state management

    View Slide

  18. Flux
    ▪ Provides clean unidirectional application flow
    ▪ Describes where state management happens (stores)
    ▪ Decouples model dependencies by using event handlers

    View Slide

  19. But flux is for the client side right?
    Let’s decompose the Flux flow to figure out how it
    could be used on the server

    View Slide

  20. Working Backwards
    React Component
    React Components

    View Slide

  21. Working Backwards
    Stores
    React Component
    React Components
    Component needs to render
    based on a certain state from
    a store

    View Slide

  22. Working Backwards
    Dispatcher
    Stores
    React Component
    React Components
    Stores are populated by
    actions from the dispatcher
    Component needs to render
    based on a certain state from
    a store

    View Slide

  23. Working Backwards
    Action Creator
    Actions are dispatched
    through the dispatcher from
    action creators
    Dispatcher
    Stores
    React Component
    React Components
    Stores are populated by
    actions from the dispatcher
    Component needs to render
    based on a certain state from
    a store

    View Slide

  24. You can achieve any application state through some set
    of action creator calls.

    View Slide

  25. Action Creator Dispatcher Stores
    React Component
    React Components
    HTTP request
    once completed
    Proposed Server Flow

    View Slide

  26. Flux Chat

    View Slide

  27. var React = require('react');
    var ChatApp = React.createFactory(require('./components/ChatApp.jsx'));
    var showMessages = require('./actions/showMessages');
    React.render(ChatApp(), document.getElementById('app'), function (err)
    {
    if (err) {
    throw err;
    }
    showMessages();
    });
    client.js


    Chat







    index.html

    View Slide

  28. var messages = require('../data/messages');
    var dispatcher = require('../lib/dispatcher');
    module.exports = function () {
    dispatcher.dispatch({
    type: 'RECEIVE_MESSAGES',
    messages: messages
    });
    };
    actions/showMessages.js
    var Dispatcher = require('flux').Dispatcher;
    module.exports = new Dispatcher();

    View Slide

  29. var dispatcher = require('../lib/dispatcher');
    var objectAssign = require('object-assign');
    var EventEmitter = require('events').EventEmitter;
    var messages = [];
    var MessageStore = objectAssign({}, EventEmitter.prototype, {
    onReceiveMessages: function (msgs) {
    messages = messages.concat(msgs);
    MessageStore.emitChange();
    },
    getAllMessages: function () {
    return messages;
    },
    });
    module.exports = MessageStore;
    stores/MessageStore.js
    dispatcher.register(function (payload) {
    switch (payload.type) {
    case 'RECEIVE_MESSAGES':
    MessageStore.onReceiveMessages(payload.
    messages);
    return;
    default:
    throw new Error('No handler found');
    }
    });

    View Slide

  30. var React = require('react');
    var MessageStore = require('../stores/MessageStore');
    var ChatApp = React.createClass({
    getInitialState: function () {
    return {
    messages: MessageStore.getAllMessages()
    }
    },
    render: function () {
    // ...
    }
    });
    module.exports = ChatApp;
    components/ChatApp.jsx

    View Slide

  31. var React = require('react');
    var ChatApp = require('./components/ChatApp.jsx');
    var showMessages= require('./actions/showMessages');
    server.get('/', function (req, res, next) {
    showMessages();
    var html = React.renderToString(ChatApp());
    res.write('Chat');
    res.write('');
    res.write('' + html + '');
    res.write('');
    res.write('')
    res.write('');
    res.end();
    });
    server.js

    View Slide

  32. What if the data is fetched asynchronously?

    View Slide

  33. var dispatcher = require('../lib/dispatcher');
    var superagent = require('superagent);
    module.exports = function (payload, done) {
    superagent
    .get('https://rawgit.com/mridgway/10be75846faa22eb6e22/raw/')
    .set('Accept', 'application/json')
    .end(function (res) {
    var messages = JSON.parse(res.text);
    dispatcher.dispatch({
    type: 'RECEIVE_MESSAGES',
    messages: messages
    });
    done();
    });
    };
    actions/showMessages.js

    View Slide

  34. server.get('/', function (req, res, next) {
    showMessages({}, function (err) {
    if (err) {
    next(err);
    return;
    }
    var html = React.renderToString(ChatApp());
    res.write('Chat');
    res.write('');
    res.write('' + html + '');
    res.write('');
    res.write('')
    res.write('');
    res.end();
    });
    });
    server.js
    add callback

    View Slide

  35. Concurrency Issues
    ▪ Data is being shared between requests
    ▪ Personal data could be leaked
    ▪ We’ll have to scope the data to the request

    View Slide

  36. Scoping the Store
    ▪ Singletons are holding global data
    ▪ Let’s make stores classes that are instantiated per
    request

    View Slide

  37. var objectAssign = require('object-assign');
    var EventEmitter = require('events').EventEmitter;
    function MessageStore() {
    this.messages = [];
    }
    objectAssign(MessageStore.prototype, EventEmitter.prototype, {
    onReceiveMessages: function (msgs) {
    this.messages = this.messages.concat(msgs);
    this.emitChange();
    },
    getAllMessages: function () {
    return this.messages;
    },
    });
    module.exports = MessageStore;
    stores/MessageStore.js

    View Slide

  38. Scoping the Dispatcher
    ▪ Now that the store is a class, registration of the handler
    needs to be done for the instance
    ▪ Dispatches should be scoped to the request too, so that
    data isn’t sent to every request’s store instance

    View Slide

  39. Dispatchr
    ▪ Register classes to the dispatcher instead of static
    functions
    ▪ Instantiate a new Dispatchr per request
    ▪ Dispatchr creates instances of stores as they are
    needed and dispatches the actions
    https://github.com/yahoo/dispatchr

    View Slide

  40. lib/dispatcher.js
    var Dispatcher = require('flux').Dispatcher;
    module.exports = new Dispatcher();
    var Dispatcher = require('dispatchr')();
    var MessageStore = require('../stores/MessageStore');
    Dispatcher.registerStore(MessageStore);
    module.exports = Dispatcher;

    View Slide

  41. var objectAssign = require('object-assign');
    var EventEmitter = require('events').EventEmitter;
    function MessageStore() {
    this.messages = [];
    }
    MessageStore.storeName = 'MessageStore';
    MessageStore.handlers = {
    'RECEIVE_MESSAGES': 'onReceiveMessages',
    'ADD_MESSAGE': 'onAddMessage'
    };
    objectAssign(MessageStore.prototype, EventEmitter.prototype, {
    onReceiveMessages: function (msgs) {
    this.messages = this.messages.concat(msgs);
    this.emitChange();
    },
    // ...
    stores/MessageStore.js

    View Slide

  42. var Dispatcher = require('./lib/dispatcher');
    server.get('/', function (req, res, next) {
    var dispatcher = new Dispatcher();
    showMessages({}, function (err) {
    if (err) {
    next(err);
    return;
    }
    var html = React.renderToString(ChatApp());
    res.write('Chat');
    res.write('');
    res.write('' + html + '');
    res.write('');
    res.write('')
    res.write('');
    res.end();
    });
    });
    server.js

    View Slide

  43. Now the showMessage action creator (and all action
    creators) need access to the dispatcher instance to call
    dispatch()

    View Slide

  44. var superagent = require('superagent);
    module.exports = function (dispatcher, payload, done) {
    superagent
    .get('https://rawgit.com/mridgway/10be75846faa22eb6e22/raw/')
    .set('Accept', 'application/json')
    .end(function (res) {
    var messages = JSON.parse(res.text);
    dispatcher.dispatch({
    type: 'RECEIVE_MESSAGES',
    messages: messages
    });
    done();
    });
    };
    actions/showMessages.js

    View Slide

  45. var Dispatcher = require('./lib/dispatcher');
    server.get('/', function (req, res, next) {
    var dispatcher = new Dispatcher();
    showMessages(dispatcher, {}, function (err) {
    if (err) {
    next(err);
    return;
    }
    var html = React.renderToString(ChatApp());
    res.write('Chat');
    res.write('');
    res.write('' + html + '');
    res.write('');
    res.write('')
    res.write('');
    res.end();
    });
    });
    server.js

    View Slide

  46. The components need to be able to retrieve state from
    the store instance

    View Slide

  47. var React = require('react');
    var MessageStore = require('../stores/MessageStore');
    var ChatApp = React.createClass({
    getInitialState: function () {
    return {
    messages: MessageStore.getAllMessages()
    }
    },
    render: function () {
    // ...
    }
    });
    module.exports = ChatApp;
    components/ChatApp.jsx

    View Slide

  48. var React = require('react');
    var MessageStore = require('../stores/MessageStore');
    var ChatApp = React.createClass({
    getInitialState: function () {
    var dispatcher = this.props.dispatcher;
    return {
    messages:dispatcher.getStore(MessagesStore).getAllMessages()
    }
    },
    render: function () {
    // ...
    }
    });
    module.exports = ChatApp;
    components/ChatApp.jsx

    View Slide

  49. var Dispatcher = require('./lib/dispatcher');
    server.get('/', function (req, res, next) {
    var dispatcher = new Dispatcher();
    showMessages(dispatcher, {}, function (err) {
    if (err) {
    next(err);
    return;
    }
    var html = React.renderToString(ChatApp({
    dispatcher: dispatcher
    }));
    res.write('Chat');
    res.write('');
    res.write('' + html + '');
    res.write('');
    res.write('')
    res.write('');
    res.end();
    });
    server.js

    View Slide

  50. Full server-side, request-scoped Flux!

    View Slide

  51. Overview of Changes
    ▪ Store instances instead of singletons
    ▪ Dispatcher instance instead of singleton
    ▪ Passing dispatcher instance to action creators and
    components that need it

    View Slide

  52. And with the same changes to the client, it all still works
    client-side too!

    View Slide

  53. var React = require('react');
    var ChatApp = require('./components/ChatApp.jsx');
    var Dispatcher = require('./lib/dispatcher');
    var dispatcher = new Dispatcher();
    var showMessages = require('./actions/showMessages');
    showMessages(dispatcher, {}, function (err) {
    if (err) {
    throw err;
    }
    React.render(ChatApp({
    dispatcher: dispatcher
    }), document.getElementById('app'), function (err) {
    if (err) {
    throw err;
    }
    });
    });
    client.js

    View Slide

  54. var React = require('react');
    var addMessage = require('../actions/addMessage');
    var AttentionButton = React.createClass({
    onClick: function () {
    addMessage(this.props.dispatcher, {
    authorName: 'Mike',
    text: 'Look at me!'
    });
    },
    render: function () {
    return (
    Demand Attention
    );
    }
    });
    module.exports = AttentionButton;
    lib/AttentionButton.js

    View Slide

  55. But why do we need to execute the action again? The
    server already got us to the state we want.
    Propagate your state from server to client!

    View Slide

  56. var html = React.renderToString(ChatApp({
    dispatcher: dispatcher
    }));
    res.expose(dispatcher.dehydrate(), 'App');
    res.write('Chat');
    res.write('');
    res.write('' + html + '');
    res.write('');
    res.write('' + res.locals.state + '');
    res.write('');
    res.write('');
    res.end();
    server.js
    var express = require('express');
    var expstate = require('express-state');
    var server = express();
    expstate.extend(server);

    View Slide

  57. function MessageStore() {
    this.messages = [];
    }
    objectAssign(MessageStore.prototype, EventEmitter.prototype, {
    onReceiveMessages: function (msgs) {
    this.messages = this.messages.concat(msgs);
    this.emitChange();
    },
    dehydrate: function () {
    return {
    messages: this.messages
    }
    },
    rehydrate: function (state) {
    this.messages = state.messages;
    }
    // ...
    stores/MessageStore.js

    View Slide

  58. var React = require('react');
    var ChatApp = React.createFactory(require('./components/ChatApp.jsx'));
    var Dispatcher = require('./lib/dispatcher');
    var dispatcher = new Dispatcher();
    dispatcher.rehydrate(window.App);
    React.render(ChatApp({
    dispatcher: dispatcher
    }), document.getElementById('app'), function (err) {
    if (err) {
    throw err;
    }
    });
    client.js

    View Slide

  59. View Slide

  60. You can see all of the code for this simplified chat
    example
    https://github.com/mridgway/isomorphic-chat

    View Slide

  61. Problems to Address

    View Slide

  62. Naming between actions, action creators, and
    component interactions can be confusing
    I’ve probably slipped up countless times throughout
    this presentation

    View Slide

  63. Dispatcher (or some scoped wrapper) needs to be
    passed around to all components that need it
    React’s undocumented context could solve this, but
    unsure of stability of the API

    View Slide

  64. Knowing which actions we should execute is based on
    which components are going to be rendered
    No way to know which components are being
    rendered. Need some step between a full component
    hierarchy and being rendered to a string.

    View Slide

  65. Fluxible Libraries

    View Slide

  66. fluxible-app
    Container for isomorphic Flux applications
    ● Wrapper around Dispatchr
    ● Provides clean APIs for constructing isomorphic flux
    applications
    ● Removes boilerplate
    ● Enforces unidirectional flow by restricting interfaces
    ● Pluggable: add new interfaces

    View Slide

  67. fluxible-plugin-routr
    Adds isomorphic routing interfaces to your fluxible-app
    ● Shared routes between server and client
    ● Shared route matching logic
    ● Create paths from components using routes names and
    parameters
    ● Example:
    context.makePath(‘user’, { id: 1 }) // returns /user/1

    View Slide

  68. fluxible-plugin-fetchr
    Adds isomorphic CRUD interfaces to your fluxible-app
    ● Create server side web services
    ● Expose them via an XHR endpoint
    ● Access your services from an action with the same code both
    server and client
    ● Example:
    context.service.read(‘messages’, {/*params*/}, callback)
    ● Client uses XHR, server calls service directly

    View Slide

  69. flux-router-component
    Provides components and mixins for handling client routing
    ● component that fires built-in `navigate` action
    creator
    ● Navigate action creator dispatches matched route
    ● App level mixin handles route changes from your store and
    triggers window pushstate

    View Slide

  70. flux-examples
    Example applications using all of the above libraries
    github.com/yahoo/flux-examples

    View Slide

  71. Links
    ▪ https://github.com/yahoo/fluxible-app
    ▪ https://github.com/yahoo/fluxible-plugin-fetchr
    ▪ https://github.com/yahoo/fluxible-plugin-routr
    ▪ https://github.com/yahoo/fetchr
    ▪ https://github.com/yahoo/dispatchr
    ▪ https://github.com/yahoo/flux-examples
    Thanks!
    Contact
    ▪ Michael Ridgway
    ▪ @TheRidgway
    [email protected]

    View Slide