Slide 1

Slide 1 text

Isomorphic Flux Michael Ridgway @TheRidgway [email protected]

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Isomorphic The ability to perform similar functionality in multiple environments

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Why Isomorphic?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

▪ 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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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/

Slide 10

Slide 10 text

Isomorphic Spectrum

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Converging Client Server Model Controller Model View Routing Routing Controller

Slide 13

Slide 13 text

Client Controller Model View Routing Server

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Achieving the Holy Grail

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Working Backwards React Component React Components

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Flux Chat

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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();

Slide 29

Slide 29 text

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'); } });

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

What if the data is fetched asynchronously?

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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;

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Full server-side, request-scoped Flux!

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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!

Slide 56

Slide 56 text

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);

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Problems to Address

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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.

Slide 65

Slide 65 text

Fluxible Libraries

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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]