Isomorphic Flux

D7969888e533ac841d12781a11d1801f?s=47 Michael Ridgway
November 06, 2014
210k

Isomorphic Flux

Talk for ReactJS SF Meetup

D7969888e533ac841d12781a11d1801f?s=128

Michael Ridgway

November 06, 2014
Tweet

Transcript

  1. 2.

    React and Flux patterns can be used to build the

    holy grail of isomorphic applications.
  2. 4.

    Agenda ▪ Why Isomorphic? ▪ Isomorphic Spectrum ▪ Achieving the

    Holy Grail ▪ Flux Chat ▪ Problems to Address
  3. 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
  4. 8.

    Why not both? All of the benefits of each AND

    we don’t have to duplicate code!
  5. 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/
  6. 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
  7. 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
  8. 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
  9. 18.

    Flux ▪ Provides clean unidirectional application flow ▪ Describes where

    state management happens (stores) ▪ Decouples model dependencies by using event handlers
  10. 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
  11. 21.
  12. 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
  13. 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
  14. 26.
  15. 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 <html> <head> <title>Chat</title> </head> <body> <div id="app"> </div> </body> <script src="./build/js/client.js"></script> </html> index.html
  16. 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();
  17. 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'); } });
  18. 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
  19. 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('<html><head><title>Chat</title></head>'); res.write('<body>'); res.write('<div id="app">' + html + '</div>'); res.write('</body>'); res.write('<script src="/build/js/client.js"></script>') res.write('</html>'); res.end(); }); server.js
  20. 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
  21. 34.

    server.get('/', function (req, res, next) { showMessages({}, function (err) {

    if (err) { next(err); return; } var html = React.renderToString(ChatApp()); res.write('<html><head><title>Chat</title></head>'); res.write('<body>'); res.write('<div id="app">' + html + '</div>'); res.write('</body>'); res.write('<script src="/build/js/client.js"></script>') res.write('</html>'); res.end(); }); }); server.js add callback
  22. 35.

    Concurrency Issues ▪ Data is being shared between requests ▪

    Personal data could be leaked ▪ We’ll have to scope the data to the request
  23. 36.

    Scoping the Store ▪ Singletons are holding global data ▪

    Let’s make stores classes that are instantiated per request
  24. 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
  25. 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
  26. 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
  27. 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;
  28. 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
  29. 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('<html><head><title>Chat</title></head>'); res.write('<body>'); res.write('<div id="app">' + html + '</div>'); res.write('</body>'); res.write('<script src="/build/js/client.js"></script>') res.write('</html>'); res.end(); }); }); server.js
  30. 43.

    Now the showMessage action creator (and all action creators) need

    access to the dispatcher instance to call dispatch()
  31. 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
  32. 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('<html><head><title>Chat</title></head>'); res.write('<body>'); res.write('<div id="app">' + html + '</div>'); res.write('</body>'); res.write('<script src="/build/js/client.js"></script>') res.write('</html>'); res.end(); }); }); server.js
  33. 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
  34. 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
  35. 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('<html><head><title>Chat</title></head>'); res.write('<body>'); res.write('<div id="app">' + html + '</div>'); res.write('</body>'); res.write('<script src="/build/js/client.js"></script>') res.write('</html>'); res.end(); }); server.js
  36. 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
  37. 52.
  38. 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
  39. 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 ( <button onClick={this.onClick}>Demand Attention</button> ); } }); module.exports = AttentionButton; lib/AttentionButton.js
  40. 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!
  41. 56.

    var html = React.renderToString(ChatApp({ dispatcher: dispatcher })); res.expose(dispatcher.dehydrate(), 'App'); res.write('<html><head><title>Chat</title></head>');

    res.write('<body>'); res.write('<div id="app">' + html + '</div>'); res.write('</body>'); res.write('<script>' + res.locals.state + '</script>'); res.write('<script src="/build/js/client.js"></script>'); res.write('</html>'); res.end(); server.js var express = require('express'); var expstate = require('express-state'); var server = express(); expstate.extend(server);
  42. 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
  43. 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
  44. 59.
  45. 60.

    You can see all of the code for this simplified

    chat example https://github.com/mridgway/isomorphic-chat
  46. 62.

    Naming between actions, action creators, and component interactions can be

    confusing I’ve probably slipped up countless times throughout this presentation
  47. 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
  48. 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.
  49. 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
  50. 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
  51. 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
  52. 69.

    flux-router-component Provides components and mixins for handling client routing •

    <NavLink> 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
  53. 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 ▪ mridgway@yahoo-inc.com