├── .npmignore ├── src ├── index.js ├── identifyAction.js ├── applyMiddleware.js └── createRenderer.js ├── .babelrc ├── .gitignore ├── .travis.yml ├── .eslintignore ├── .flowconfig ├── .jscsrc ├── .eslintrc ├── test ├── testHelper.js ├── identifyAction.spec.js ├── applyMiddleware.spec.js └── createRenderer.spec.js ├── webpack.config.js ├── LICENSE.md ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | npm-debug.log 4 | coverage 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default from './applyMiddleware'; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | sudo: false 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | **/node_modules 3 | **/webpack.config.js 4 | **/coverage 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | **/coverage/* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [options] 9 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "excludeFiles": [ 4 | "coverage/**", 5 | "node_modules/**", 6 | "lib/**" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb/base", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/testHelper.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinonChai from 'sinon-chai'; 3 | import chaiAsPromised from 'chai-as-promised'; 4 | 5 | chai.use(sinonChai); 6 | chai.use(chaiAsPromised); 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | module: { 5 | loaders: [ 6 | { 7 | test: /\.js$/, 8 | loaders: ['babel-loader'], 9 | exclude: /node_modules/, 10 | }, 11 | ], 12 | }, 13 | resolve: { 14 | extensions: ['', '.js'], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/identifyAction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Identify a given action. 3 | * 4 | * If the passed action is actually a function (e.g. when using the `thunk` 5 | * middleware) this creates a hased representation of the function body. If it 6 | * is a standard flux action with a type property, the type is returned. 7 | */ 8 | export default (action) => { 9 | if (typeof action !== 'function') { 10 | return action.type; 11 | } 12 | 13 | let hash = 0; 14 | const string = action.toString(); 15 | for (const index of string) { 16 | const value = ((hash << 5) - hash) + string.charCodeAt(index); 17 | hash = value & value; 18 | } 19 | 20 | return Math.abs(hash); 21 | }; 22 | -------------------------------------------------------------------------------- /test/identifyAction.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import identifyAction from '../src/identifyAction'; 3 | 4 | describe('identifyAction', () => { 5 | it('should returns the action\'s type in case the action is an object', () => { 6 | const action = { 7 | type: 'DO_SOMETHING', 8 | }; 9 | expect(identifyAction(action)).to.equal('DO_SOMETHING'); 10 | }); 11 | 12 | it('should return the hash of the function\'s body in case the action is a function', () => { 13 | const actionA = () => 42; 14 | const actionB = () => 41; 15 | expect(identifyAction(actionA)).to.equal(1475036588); 16 | expect(identifyAction(actionB)).to.equal(789807045); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sebastian Siemssen & Nik Graf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/applyMiddleware.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import applyMiddleware from '../src/applyMiddleware'; 3 | import { createStore } from 'redux'; 4 | 5 | const instantiateStore = (middlewares) => { 6 | const storeEnhancers = applyMiddleware(...middlewares); 7 | const enhancedCreateStore = storeEnhancers(createStore); 8 | const initialState = { 9 | isLoading: false, 10 | }; 11 | const reducer = (state) => state; 12 | return enhancedCreateStore(reducer, initialState); 13 | }; 14 | 15 | describe('a store decorated with the custom applyMiddleware', () => { 16 | it('should contain the renderUniversal function', () => { 17 | const store = instantiateStore([]); 18 | expect(store.renderUniversal).to.be.a('function'); 19 | }); 20 | 21 | it('should apply the passed middlewares', (done) => { 22 | const middleware = () => () => () => { 23 | done(); 24 | }; 25 | 26 | const store = instantiateStore([middleware]); 27 | store.dispatch({ type: 'DO_SOMETHING' }); 28 | }); 29 | 30 | it('should wait for generated promises when calling renderUniversal', () => { 31 | let counter = 0; 32 | 33 | const middleware = () => () => (action) => { 34 | return new Promise((resolve) => { 35 | setTimeout(() => resolve(action), 25); 36 | }); 37 | }; 38 | 39 | const store = instantiateStore([middleware]); 40 | 41 | const render = () => { 42 | if (counter === 0) { 43 | store.dispatch({ type: 'DO_SOMETHING' }); 44 | counter++; 45 | } 46 | 47 | return 'output'; 48 | }; 49 | 50 | return expect(store.renderUniversal(render)).to.eventually.have.property('output'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/applyMiddleware.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware as reduxApplyMiddleware } from 'redux'; 2 | import createRenderer from './createRenderer'; 3 | import isPromise from 'is-promise'; 4 | 5 | /** 6 | * Decorates all middlewares before passing them to the original implementation. 7 | * 8 | * This allows us to catch promises returned at any point during a dispatch. We 9 | * can then add those to the promise store for delaying the rendering of the 10 | * output in the universal renderer. 11 | */ 12 | export default (...middlewares) => { 13 | // List of promises. 14 | const activePromises = []; 15 | 16 | // Adds a promise to the list of promises to watch. 17 | const addPromise = (promise, action) => { 18 | const item = [promise, action]; 19 | activePromises.push(item); 20 | 21 | const removePromise = () => { 22 | const index = activePromises.indexOf(item); 23 | 24 | if (index !== -1) { 25 | activePromises.splice(index, 1); 26 | } 27 | }; 28 | 29 | // Remove the promise from the list when it's fulfilled or rejected. 30 | promise.then(removePromise, removePromise); 31 | 32 | return removePromise; 33 | }; 34 | 35 | // Returns the list of currently watched promises. 36 | const getPromises = () => activePromises.slice(); 37 | 38 | // Decorate all passed middlewares. 39 | const decoratedMiddlewares = middlewares.map((original) => (api) => (next) => ((decorated) => (action) => { 40 | const result = decorated(action); 41 | if (isPromise(result)) { 42 | addPromise(result, action); 43 | } 44 | 45 | return result; 46 | })(original(api)(next))); 47 | 48 | return (next) => (reducer, initialState) => ({ 49 | ...reduxApplyMiddleware(...decoratedMiddlewares)(next)(reducer, initialState), 50 | renderUniversal: createRenderer(getPromises), 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-universal", 3 | "version": "0.0.2", 4 | "description": "A replacement for Redux's built-in middleware store enhancer to build universal apps.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib", 8 | "build": "npm run clean && babel src --out-dir lib", 9 | "test": "mocha test --recursive --require ./test/testHelper.js --compilers js:babel/register", 10 | "test:watch": "npm run test -- --watch", 11 | "test:cov": "babel-node ./node_modules/.bin/isparta cover --report=html _mocha -- --recursive --require ./test/testHelper.js --compilers js:babel/register", 12 | "test:cov:open": "npm run test:cov && open ./coverage/index.html", 13 | "lint": "npm run lint:eslint && npm run lint:jscs && npm run lint:flow", 14 | "lint:flow": "flow", 15 | "lint:eslint": "eslint ./", 16 | "lint:jscs": "jscs ./", 17 | "prepublish": "npm run lint && npm run test && npm run build" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/reducks/redux-universal.git" 22 | }, 23 | "keywords": [ 24 | "react", 25 | "redux", 26 | "universal", 27 | "isomorphic" 28 | ], 29 | "authors": [ 30 | "Sebastian Siemssen ", 31 | "Nik Graf " 32 | ], 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/reducks/redux-universal/issues" 36 | }, 37 | "homepage": "https://github.com/reducks/redux-universal", 38 | "dependencies": { 39 | "is-promise": "^2.1.0", 40 | "redux": "^3.0.0" 41 | }, 42 | "devDependencies": { 43 | "babel": "^5.8.23", 44 | "babel-core": "^5.8.25", 45 | "babel-eslint": "^4.1.3", 46 | "babel-loader": "^5.3.2", 47 | "chai": "^3.3.0", 48 | "chai-as-promised": "^5.1.0", 49 | "eslint": "^1.5.1", 50 | "eslint-config-airbnb": "^0.1.0", 51 | "flow-bin": "^0.16.0", 52 | "isparta": "^3.1.0", 53 | "istanbul": "^0.3.21", 54 | "jscs": "^2.2.1", 55 | "mocha": "^2.3.3", 56 | "rimraf": "^2.4.3", 57 | "sinon": "^1.17.1", 58 | "sinon-chai": "^2.8.0", 59 | "webpack": "^1.12.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/createRenderer.js: -------------------------------------------------------------------------------- 1 | import identifyAction from './identifyAction'; 2 | 3 | const repeatedActionError = (repeatedActionBodies) => { 4 | return new Error([ 5 | `Rendering has been aborted to prevent an infinite loop. `, 6 | `The following asynchronous actions were called repeatedly in `, 7 | `successive rendering cycles: `, 8 | ...repeatedActionBodies.map((action) => { 9 | if (typeof action === 'function') { 10 | return action.toString(); 11 | } 12 | 13 | return require('util').inspect(action, { 14 | depth: 2, 15 | }); 16 | }), 17 | ].join('')); 18 | }; 19 | 20 | /** 21 | * Creates a universal render function. 22 | * 23 | * By interacting with the passed promise watcher and the store it ensures that 24 | * the rendering reiteratively continues until no further actions are dispatched 25 | * and all current promises are resolved. 26 | * 27 | * The inner workings of the repetitive rendering require that each watched 28 | * promise also dispatches an action when it is resolved or rejected. Using it 29 | * together with a middleware that handles promises from synchronous action 30 | * creators is encouraged. 31 | */ 32 | export default (getPromises) => { 33 | return (renderFn, element) => { 34 | return new Promise((resolve, reject) => { 35 | let seenPromises = []; 36 | let seenActions = {}; 37 | let output = ''; 38 | 39 | const render = () => { 40 | try { 41 | // Invoke the actual render function (e.g. ReactDOM.renderToString). 42 | output = renderFn(element); 43 | const activePromises = getPromises(); 44 | 45 | const [ newPromises, newActions ] = activePromises 46 | .filter(([ promise ]) => seenPromises.indexOf(promise) === -1) 47 | .reduce(([ promiseA, actionA ], [ promiseB, actionB ]) => ([ 48 | promiseA.concat(promiseB), 49 | actionA.concat(actionB), 50 | ]), [[], []]); 51 | 52 | const actionsWithoutDuplicates = newActions.reduce((map, action) => ({ 53 | ...map, [identifyAction(action)]: action, 54 | }), {}); 55 | 56 | const repeatedActionBodies = Object.keys(actionsWithoutDuplicates) 57 | .filter((key) => seenActions.hasOwnProperty(key)) 58 | .map((key) => actionsWithoutDuplicates[key]); 59 | 60 | if (repeatedActionBodies.length) { 61 | throw repeatedActionError(repeatedActionBodies); 62 | } else { 63 | seenPromises = seenPromises.concat(newPromises); 64 | seenActions = {...seenActions, ...actionsWithoutDuplicates}; 65 | 66 | if (!activePromises.length) { 67 | resolve({ output }); 68 | } else { 69 | // If any promises are left, re-render once the first promise has 70 | // been either resolved or rejected. 71 | Promise.race(activePromises.map(([ promise ]) => promise)).then(render, render); 72 | } 73 | } 74 | } catch (error) { 75 | reject({ output, error }); 76 | } 77 | }; 78 | 79 | render(); 80 | }); 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /test/createRenderer.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import createRenderer from '../src/createRenderer'; 3 | 4 | const createPromise = (milliseconds) => { 5 | return new Promise((resolve) => { 6 | setTimeout(() => resolve(), milliseconds); 7 | }); 8 | }; 9 | 10 | describe('createRenderer', () => { 11 | let renderCalledCounter; 12 | 13 | const render = () => { 14 | renderCalledCounter++; 15 | return 'output'; 16 | }; 17 | 18 | beforeEach(() => { 19 | renderCalledCounter = 0; 20 | }); 21 | 22 | it('should resolve right away in case no pormises are provided', () => { 23 | const renderUniversal = createRenderer(() => []); 24 | 25 | return renderUniversal(render).then((result) => { 26 | expect(renderCalledCounter).to.equal(1); 27 | expect(result.output).to.equal('output'); 28 | }); 29 | }); 30 | 31 | it('should resolve after all provided promises are resolved', () => { 32 | const promiseA = createPromise(20); 33 | const promiseB = createPromise(10); 34 | const promiseC = createPromise(30); 35 | 36 | const renderUniversal = createRenderer(() => { 37 | if (renderCalledCounter === 1) { 38 | return [ 39 | [promiseA, { type: 'DO_THIS' }], 40 | [promiseB, { type: 'DO_THAT' }], 41 | [promiseC, { type: 'DO_SOMETHING' }], 42 | ]; 43 | } 44 | 45 | return []; 46 | }); 47 | 48 | return renderUniversal(render).then((result) => { 49 | expect(promiseA).to.be.fulfilled; // eslint-disable-line no-unused-expressions 50 | expect(promiseB).to.be.fulfilled; // eslint-disable-line no-unused-expressions 51 | expect(promiseC).to.be.fulfilled; // eslint-disable-line no-unused-expressions 52 | expect(renderCalledCounter).to.equal(2); 53 | expect(result.output).to.equal('output'); 54 | }); 55 | }); 56 | 57 | it('should resolve after all promises from a first and second render call', () => { 58 | const promiseA = createPromise(20); 59 | const promiseB = createPromise(10); 60 | 61 | const renderUniversal = createRenderer(() => { 62 | if (renderCalledCounter === 1) { 63 | return [[promiseA, { type: 'DO_THIS' }]]; 64 | } else if (renderCalledCounter === 2) { 65 | return [[promiseB, { type: 'DO_THAT' }]]; 66 | } 67 | 68 | return []; 69 | }); 70 | 71 | return renderUniversal(render).then((result) => { 72 | expect(promiseA).to.be.fulfilled; // eslint-disable-line no-unused-expressions 73 | expect(promiseB).to.be.fulfilled; // eslint-disable-line no-unused-expressions 74 | expect(renderCalledCounter).to.equal(3); 75 | expect(result.output).to.equal('output'); 76 | }); 77 | }); 78 | 79 | it('should bail out after seeing the same action type a second time to prevent an infinite loop', () => { 80 | const promiseA = createPromise(20); 81 | const promiseB = createPromise(10); 82 | 83 | const renderUniversal = createRenderer(() => { 84 | if (renderCalledCounter === 1) { 85 | return [[promiseA, { type: 'DO_THIS' }]]; 86 | } else if (renderCalledCounter === 2) { 87 | return [[promiseB, { type: 'DO_THIS' }]]; 88 | } 89 | 90 | return []; 91 | }); 92 | 93 | return renderUniversal(render).catch((result) => { 94 | expect(promiseA).to.be.fulfilled; // eslint-disable-line no-unused-expressions 95 | expect(promiseB).to.be.fulfilled; // eslint-disable-line no-unused-expressions 96 | expect(renderCalledCounter).to.equal(2); 97 | expect(result.output).to.equal('output'); 98 | }); 99 | }); 100 | 101 | it('should bail out after seeing the same function body a second time to prevent an infinite loop', () => { 102 | const promiseA = createPromise(20); 103 | const promiseB = createPromise(10); 104 | 105 | const renderUniversal = createRenderer(() => { 106 | if (renderCalledCounter === 1) { 107 | return [[promiseA, () => 42]]; 108 | } else if (renderCalledCounter === 2) { 109 | return [[promiseB, () => 42]]; 110 | } 111 | 112 | return []; 113 | }); 114 | 115 | return renderUniversal(render).catch((result) => { 116 | expect(promiseA).to.be.fulfilled; // eslint-disable-line no-unused-expressions 117 | expect(promiseB).to.be.fulfilled; // eslint-disable-line no-unused-expressions 118 | expect(renderCalledCounter).to.equal(2); 119 | expect(result.output).to.equal('output'); 120 | }); 121 | }); 122 | 123 | it('should be rejected in case no render functions is provided', () => { 124 | const renderUniversal = createRenderer(() => []); 125 | return expect(renderUniversal()).to.be.rejected; 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **INACTIVE** This library is not maintained 2 | 3 | # Redux Universal 4 | 5 | A Redux store enhancer taking care of promise resolution for building universal apps. 6 | 7 | ## Background 8 | 9 | Rendering a Redux application on the server requires you to give up on certain programming patterns. For once you can't use singletons on the server as they would be shared between multiple users. Another issue we encountered is resolving Promises trigger from actions within components. While many Redux boilerplates use custom routers to handle data fetching on the client & server we decided to approach it differently. Our goal was to surface a minimal API that can be plugged into any existing Redux application without adding limitations in the way people built their applications. 10 | 11 | ## Setup 12 | 13 | To install the stable version run: 14 | 15 | ``` 16 | npm install --save redux-univeral 17 | ``` 18 | 19 | In the file where you configure your store you need to make sure to use the applyMiddleware when rendering in the backend. 20 | 21 | ``` 22 | const applyMiddleware = __SERVER__ ? 23 | require('redux-universal') : 24 | require('redux').applyMiddleware; 25 | ``` 26 | 27 | The custom applyMiddleware enhances the store and appends a new method to it. 28 | 29 | ``` 30 | store.renderUniversal(ReactDOM.renderToString, ) 31 | .then(({ output }) => { 32 | response.send(output); 33 | }) 34 | .catch(({ output, error }) => { 35 | console.warn(error.message); 36 | response.send(output); 37 | }); 38 | ``` 39 | 40 | 41 | ## How it Works 42 | 43 | Redux Universal will catch any Promise returned by a middleware. The action itself can be a function (redux-thunk) or an object with a type (redux-catch-promise). Calling `renderUniversal` returns a Promise which is fulfilled once all Promises are resolved. 44 | 45 | ## Guide with Redux-catch-promise 46 | 47 | TODO 48 | 49 | ## Guide with Redux-thunk 50 | 51 | Cities.js 52 | 53 | Dispatch the fetchCities action once the Components gets initialized. 54 | 55 | ``` 56 | class Cities extends Component { 57 | 58 | componentWillMount() { 59 | this.props.dispatch(fetchCities()); 60 | } 61 | 62 | render() { 63 | return
Cities
; 64 | } 65 | } 66 | ``` 67 | 68 | CityActions.js 69 | 70 | fetchCities returns a Promise which will fulfill once the response came in and is converted to JSON. 71 | 72 | ``` 73 | const requestCities = () => { type: REQUEST_CITIES }; 74 | const receiveCities = (json) => { type: RECEIVE_CITIES, payload: json }; 75 | 76 | export function fetchCities() { 77 | return (dispatch) => { 78 | dispatch(requestCities()); 79 | 80 | return fetch('http://cities.example.com') 81 | .then(response => response.json()) 82 | .then(json => dispatch(receiveCities(json))); 83 | }; 84 | } 85 | ``` 86 | 87 | configureStore.js 88 | 89 | Depending on if the application is rendered on the server or client the normal Redux's applyMiddleware or Redux-universal's applyMiddleware must be used. 90 | 91 | ``` 92 | import reducer from '../reducers/index'; 93 | import thunkMiddleware from 'redux-thunk'; 94 | const applyMiddleware = __SERVER__ ? 95 | require('redux-universal') : 96 | require('redux').applyMiddleware; 97 | 98 | const createStoreWithMiddlewares = applyMiddleware(thunkMiddleware)(createStore); 99 | 100 | export default function configureStore(initialState = {}) { 101 | return createStoreWithMiddlewares(reducer, initialState); 102 | } 103 | ``` 104 | 105 | server.js 106 | ``` 107 | function renderHtml(html, initialState) { 108 | return ` 109 | 110 | 111 | 112 | 113 | 114 |
${html}
115 | 118 | 119 | 120 | `; 121 | } 122 | 123 | const app = express(); 124 | 125 | app.use((request, response) => { 126 | const history = createHistory({ 127 | getCurrentLocation: () => createLocation(request.path, {}, undefined, 'root'), 128 | }); 129 | 130 | // Create a new Redux store instance 131 | const store = configureStore(); 132 | const rootComponent = (); 133 | 134 | store.renderUniversal(ReactDOMServer.renderToString, rootComponent) 135 | .then(({ output }) => { 136 | const state = store.getState(); 137 | response.send(renderHtml(html, state)); 138 | }) 139 | .catch(({ output, error }) => { 140 | const state = store.getState(); 141 | response.send(renderHtml(html, state)); 142 | }); 143 | }); 144 | 145 | app.listen(8080); 146 | ``` 147 | 148 | ## Limitations 149 | 150 | To prevent endless loops there is a mechanism to detect in case the same action 151 | is triggered multiple times. In this case the promise is rejected. While this 152 | works pretty well we still recommend to write your application in a way that 153 | double fetching won't be caused by rendering the app multiple times. 154 | 155 | ## Why (Isomorphic) Universal rendering? 156 | 157 | 1. Faster Perceived Load Time 158 | 159 | The network roundtrips to load all the resources take time. By already pre-rendering the first page impression the user experience can be improved. This becomes even more important in places with high internet latency. 160 | 161 | 2. Search Engine Indexability 162 | 163 | Many search engines only rely on server side rendering for indexing. Google already improved their search crawler to index client side rendered content, but they still struggle challenges according to this [Angular2 document](https://docs.google.com/document/d/1q6g9UlmEZDXgrkY88AJZ6MUrUxcnwhBGS0EXbVlYicY). By rendering the page on the server you simply 164 | 165 | 3. Code Reusability & Maintainability 166 | 167 | Libraries can be shared between the backend & front-end. 168 | 169 | ## Client side vs Universal rendering 170 | 171 | ### Use case with client side rendering 172 | 173 | - (Client) Request the website's HTML 174 | - (Server) Serve the page without content 175 | - (Client) Request JavaScript code based on sources in the HTML 176 | - (Server) Serve the JavaScript code 177 | - (Client) Load & execute JavaScript 178 | - (Client) -> Render a loading page 179 | - (Client) Request data based on the executed code 180 | - (Server) Collect and serve the data 181 | - (Client) -> Render the content 182 | 183 | Caching definitely helps to reduce the loading times and can be done easily for the HTML as well as for the code. 184 | 185 | ### Use case with universal rendering 186 | 187 | JavaScript code is already loaded when starting the server. From experience I saw this can take a couple hundred milliseconds. 188 | 189 | - (Client) Request the website's HTML 190 | - (Server) Execute the JavaScript Code 191 | - (Server) Collect the data 192 | - (Server) Render the page in the backend 193 | - (Server) Serve the page with content 194 | - (Client) -> Render the content 195 | - (Client) Request JavaScript code based on sources in the HTML 196 | - (Server) Serve the JavaScript code 197 | - (Client) Load & execute JavaScript 198 | 199 | ## Pros & Cons 200 | 201 | While with the initial site can be serve faster with client-side rendering there is no relvant content for the user. The network roundtrips increase the time until the user actually sees relevant content. While with the universal approach it takes a bit longer until the user receives the first page it already comes with the content and the total loading time is faster. 202 | 203 | ### More resource 204 | 205 | - https://strongloop.com/strongblog/node-js-react-isomorphic-javascript-why-it-matters/ 206 | - http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/ 207 | - https://docs.google.com/document/d/1q6g9UlmEZDXgrkY88AJZ6MUrUxcnwhBGS0EXbVlYicY/edit 208 | 209 | 210 | ## Initial Technical Requirements for redux-universal 211 | 212 | It should just work out of the box without changing your front-end code. No special routing or restrictions should be needed. 213 | 214 | All tools must be ready to work with server-side rendering. Luckily that's the case in the React/Redux eco-sytem: 215 | 216 | - React (supports server side rendering) 217 | - React-Router 218 | - Redux (Flux from Facebook uses singletons which makes it hard to use on the backend) 219 | - webpack or browserify 220 | - ismorphic-fetch 221 | 222 | ## License 223 | 224 | MIT 225 | --------------------------------------------------------------------------------