Pro Yearly is on sale from $80 to $50! »

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. Isomorphic Flux Michael Ridgway @TheRidgway mridgway@yahoo-inc.com

  2. React and Flux patterns can be used to build the

    holy grail of isomorphic applications.
  3. Isomorphic The ability to perform similar functionality in multiple environments

  4. Agenda ▪ Why Isomorphic? ▪ Isomorphic Spectrum ▪ Achieving the

    Holy Grail ▪ Flux Chat ▪ Problems to Address
  5. Why Isomorphic?

  6. Server Rendering ▪ Search engine optimization ▪ Legacy browser support

    ▪ User perceived performance
  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
  8. Why not both? All of the benefits of each AND

    we don’t have to duplicate code!
  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/
  10. Isomorphic Spectrum

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

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

  13. Client Controller Model View Routing Server

  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
  15. Achieving the Holy Grail

  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
  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
  18. Flux ▪ Provides clean unidirectional application flow ▪ Describes where

    state management happens (stores) ▪ Decouples model dependencies by using event handlers
  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
  20. Working Backwards React Component React Components

  21. Working Backwards Stores React Component React Components Component needs to

    render based on a certain state from a store
  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
  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
  24. You can achieve any application state through some set of

    action creator calls.
  25. Action Creator Dispatcher Stores React Component React Components HTTP request

    once completed Proposed Server Flow
  26. Flux Chat

  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
  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();
  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'); } });
  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
  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
  32. What if the data is fetched asynchronously?

  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
  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
  35. Concurrency Issues ▪ Data is being shared between requests ▪

    Personal data could be leaked ▪ We’ll have to scope the data to the request
  36. Scoping the Store ▪ Singletons are holding global data ▪

    Let’s make stores classes that are instantiated per request
  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
  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
  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
  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;
  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
  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
  43. Now the showMessage action creator (and all action creators) need

    access to the dispatcher instance to call dispatch()
  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
  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
  46. The components need to be able to retrieve state from

    the store instance
  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
  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
  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
  50. Full server-side, request-scoped Flux!

  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
  52. And with the same changes to the client, it all

    still works client-side too!
  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
  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
  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!
  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);
  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
  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
  59. None
  60. You can see all of the code for this simplified

    chat example https://github.com/mridgway/isomorphic-chat
  61. Problems to Address

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

    confusing I’ve probably slipped up countless times throughout this presentation
  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
  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.
  65. Fluxible Libraries

  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
  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
  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
  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
  70. flux-examples Example applications using all of the above libraries github.com/yahoo/flux-examples

  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