├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.json ├── bin └── server.js ├── circle.yml ├── karma.conf.js ├── package.json ├── server.babel.js ├── src ├── client.js ├── components │ ├── SSRCachingSimpleType.js │ ├── SSRCachingTemplateType.js │ ├── above-the-fold.js │ └── home.js ├── config.js ├── helpers │ └── Html.js ├── routes.js ├── server.js └── theme │ ├── bootstrap.config.js │ ├── bootstrap.config.prod.js │ ├── bootstrap.overrides.scss │ ├── font-awesome.config.js │ ├── font-awesome.config.less │ ├── font-awesome.config.prod.js │ └── variables.scss ├── static ├── favicon.ico ├── favicon.png └── logo.jpg ├── tests.webpack.js └── webpack ├── dev.config.js ├── prod.config.js ├── webpack-dev-server.js └── webpack-isomorphic-tools.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | 4 | "plugins": [ 5 | "transform-runtime", 6 | "add-module-exports", 7 | "transform-decorators-legacy", 8 | "transform-react-display-name" 9 | ], 10 | 11 | "env": { 12 | "development": { 13 | "plugins": [ 14 | "typecheck", 15 | ["react-transform", { 16 | "transforms": [{ 17 | "transform": "react-transform-catch-errors", 18 | "imports": ["react", "redbox-react"] 19 | } 20 | ] 21 | }] 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | karma.conf.js 3 | tests.webpack.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": "eslint-config-airbnb", 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "react/no-multi-comp": 0, 9 | "import/default": 0, 10 | "import/no-duplicates": 0, 11 | "import/named": 0, 12 | "import/namespace": 0, 13 | "import/no-unresolved": 0, 14 | "import/no-named-as-default": 2, 15 | "comma-dangle": 0, // not sure why airbnb turned this on. gross! 16 | "indent": [2, 2, {"SwitchCase": 1}], 17 | "no-console": 0, 18 | "no-alert": 0 19 | }, 20 | "plugins": [ 21 | "react", "import" 22 | ], 23 | "settings": { 24 | "import/parser": "babel-eslint", 25 | "import/resolve": { 26 | "moduleDirectory": ["node_modules", "src"] 27 | } 28 | }, 29 | "globals": { 30 | "__DEVELOPMENT__": true, 31 | "__CLIENT__": true, 32 | "__SERVER__": true, 33 | "__DISABLE_SSR__": true, 34 | "__DEVTOOLS__": true, 35 | "socket": true, 36 | "webpackIsomorphicTools": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-assets.json 6 | webpack-stats.json 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.12" 5 | - "4.0" 6 | - "4" 7 | - "5" 8 | - "stable" 9 | 10 | sudo: false 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | 16 | script: 17 | - npm run lint 18 | - npm test 19 | - npm run test-node 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #459 refactor(utils): create url mapper utility function 29 | #463 chore(webpack): update to isomorphic tools v2 30 | #494 fix(babel): correct dependencies and polyfills 31 | #510 feat(app): add react-bootstrap responsive navbar 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `helpers`, `api` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Rasmussen 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux Universal Hot Example 2 | 3 | This repo is a sample Express Universal app with the following Electrode modules: 4 | - [Electrode React SSR Caching](https://github.com/electrode-io/electrode-react-ssr-caching) 5 | - [Electrode Redux Router Engine](https://github.com/electrode-io/electrode-redux-router-engine) 6 | - [Above The Fold Only Server Render](https://github.com/electrode-io/above-the-fold-only-server-render) 7 | 8 | --- 9 | 10 | ## About 11 | 12 | This application was originally cloned from [erikras/react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example). We have demonstrated the use of 3 features of Electrode Stand Alone modules: 13 | 14 | * [Electrode Server Side Rendering component caching](https://github.com/electrode-io/electrode-react-ssr-caching) 15 | * [Electrode Redux Router Engine](https://github.com/electrode-io/electrode-redux-router-engine) 16 | * [Electrode Above the fold only server render](https://github.com/electrode-io/above-the-fold-only-server-render) 17 | 18 | ## Installation 19 | 20 | ``` 21 | npm install 22 | ``` 23 | 24 | ## Running Server in Production Mode 25 | 26 | ``` 27 | npm run build 28 | npm run start 29 | ``` 30 | --- 31 | ## Electrode Server Side Rendering Component Caching ## 32 | 33 | [Electrode-react-ssr-caching](https://github.com/electrode-io/electrode-react-ssr-caching) module supports profiling React Server Side Rendering time and component caching to help you speed up SSR. 34 | 35 | It supports 2 types of caching: 36 | 37 | * Simple - Component Props become the cache key. This is useful for cases like Header and Footer, where the number of variations of props data is minimal which will make sure the cache size stays small. 38 | * Template - Components Props are first tokenized and then the generated template html is cached. The idea is akin to generating logic-less handlebars template from your React components and then use string replace to process the template with different props. This is useful for cases like displaying Product information in a Carousel where you have millions of products in the repository. 39 | 40 | ### Install 41 | ```bash 42 | $ npm install --save electrode-react-ssr-caching 43 | ``` 44 | 45 | ### Wiring 46 | 47 | #### GOTCHA 48 | - SSR caching of components only works in PRODUCTION mode, since the props(which are read only) are mutated for caching purposes and mutating of props is not allowed in development mode by react. 49 | 50 | - Make sure the `electrode-react-ssr-caching` module is imported first followed by the imports of react and react-dom module. SSR caching will not work if the ordering is changed since caching module has to have a chance to patch react's code first. Also if you are importing `electrode-react-ssr-caching`, `react` and `react-dom` in the same file , make sure you are using all `require` or all `import`. Found that SSR caching was NOT working if, `electrode-react-ssr-caching` is `require`d first and then `react` and `react-dom` is imported. 51 | 52 | To demonstrate functionality, we have added: 53 | 54 | * `client/components/SSRCachingSimpleType.jsx` for Simple strategy. 55 | * `client/components/SSRCachingTemplateType.jsx` for Template strategy. 56 | * To enable caching using `electrode-react-ssr-caching`, we need to do the below configuration: 57 | 58 | ```javascript 59 | const cacheConfig = { 60 | components: { 61 | SSRCachingTemplateType: { 62 | strategy: "template", 63 | enable: true 64 | }, 65 | SSRCachingSimpleType: { 66 | strategy: "simple", 67 | enable: true 68 | } 69 | } 70 | }; 71 | 72 | SSRCaching.enableCaching(); 73 | SSRCaching.setCachingConfig(cacheConfig); 74 | ``` 75 | 76 | The above configuration is done in `server/index.js`. 77 | 78 | To read more, go to [electrode-react-ssr-caching](https://github.com/electrode-io/electrode-react-ssr-caching). The core implementation for caching is [available here](https://github.com/electrode-io/electrode-react-ssr-caching/blob/master/lib/ssr-caching.js). You can also do [Profiling of components](https://github.com/electrode-io/electrode-react-ssr-caching#profiling) 79 | 80 | --- 81 | ## Electrode Redux Router Engine ## 82 | 83 | [Redux Router Engine](https://github.com/electrode-io/electrode-redux-router-engine) handles async data for React Server Side Rendering using [react-router], Redux, and the [Redux Server Rendering] pattern. 84 | 85 | The wiring is done in the following way: 86 | 87 | > Creation of REDUX store which returns a promise eventually resolving to a constructed redux store 88 | 89 | ```javascript 90 | 91 | function createReduxStore(req, match) { 92 | let initialState = {count : 100}; 93 | let rootReducer = (s, a) => s 94 | store = createStore(rootReducer, initialState); 95 | 96 | return Promise.all([ 97 | // DO ASYNC THUNK ACTIONS HERE : store.dispatch(boostrapApp()) 98 | Promise.resolve({}) 99 | ]).then(() => { 100 | return store; 101 | }); 102 | } 103 | ``` 104 | 105 | >Initialization of ReduxRouterEngine with routes and createReduxStore function 106 | 107 | ```javascript 108 | const engine = new ReduxRouterEngine({ routes, createReduxStore}); 109 | ``` 110 | >Render the html for the given request 111 | 112 | ```javascript 113 | engine.render(req) 114 | .then( (result) => { 115 | // send full HTML with result back using res 116 | }); 117 | ``` 118 | --- 119 | ## Above The Fold Only Server Render 120 | [Above The Fold Only Server Render](https://github.com/electrode-io/above-the-fold-only-server-render) is a React component for optionally skipping server side rendering of components outside above-the-fold (or inside of the viewport). This component helps render your components on the server that are above the fold and the remaining components on the client. 121 | 122 | [Above-the-fold-only-server-render](https://github.com/electrode-io/above-the-fold-only-server-render) helps increase performance both by decreasing the load on renderToString and sending the end user a smaller amount of markup. 123 | 124 | By default, the [above-the-fold-only-server-render](https://github.com/electrode-io/above-the-fold-only-server-render) component is an exercise in simplicity; it does nothing and only returns the child component. 125 | 126 | ### Install 127 | - Add the `above-the-fold-only-server-render` component: 128 | 129 | ```bash 130 | npm install above-the-fold-only-server-render --save 131 | ``` 132 | 133 | You can tell the component to skip server side rendering either by passing a `prop` `skip={true}` or by setting up `skipServerRender` in your app context and passing the component a `contextKey` `prop`. 134 | 135 | Let's explore passing `skip prop`; there is an example in 136 | `/components/above-fold-simple.jsx`. On the Home page, click the link to render the `localhost:3000/above-the-fold` page. 137 | 138 | The best way to demo this existing component is actually going to be in your `node_modules.` 139 | 140 | Navigate to `/node_modules/above-the-fold-only-server-render/lib/components/above-the-fold-only-server-render.js` line 29: 141 | 142 | ```javascript 143 | var SHOW_TIMEOUT = 50; 144 | ``` 145 | 146 | When we use this module at [WalmartLabs](www.walmartlabs.com), it's all about optimization. You are going to change line 29 to slow down the SHOW_TIMEOUT so you can see the component wrapper in action: 147 | Change this to: 148 | 149 | ```javascript 150 | var SHOW_TIMEOUT = 3000; 151 | ``` 152 | 153 | Run the commands below and test it out in your app: 154 | 155 | ```bash 156 | npm run build && npm run start 157 | ``` 158 | 159 | The code in the `

` tags that are above and below the ` ` will render first: 160 | 161 | ```javascript 162 | import React from "react"; 163 | import {AboveTheFoldOnlyServerRender} from "above-the-fold-only-server-render"; 164 | 165 | export class AboveFold extends React.Component { 166 | 167 | render() { 168 | return ( 169 |
170 |

Above-the-fold-only-server-render: Increase Your Performance

171 | 172 |
173 |

This will skip server rendering if the 'AboveTheFoldOnlyServerRender' 174 | lines are present, or uncommented out.

175 |

This will be rendered on the server and visible if the 'AboveTheFoldOnlyServerRender' 176 | lines are commented out.

177 |

Try manually toggling this component to see it in action

178 |

179 | Read more about this module and see our live demo 181 | 182 |

183 |
184 |
185 |

This is below the 'Above the fold closing tag'

186 |
187 | ); 188 | } 189 | } 190 | ``` 191 | 192 | You can also skip server side rendering by `setting context in your app and passing a contextKey prop`. Here is an example: 193 | 194 | ```js 195 | 196 | const YourComponent = () => { 197 | return ( 198 | 199 |
This will not be server side rendered based on the context.
200 |
201 | ); 202 | }; 203 | 204 | class YourApp extends React.Component { 205 | getChildContext() { 206 | return { 207 | aboveTheFoldOnlyServerRender: { 208 | YourComponent: true 209 | } 210 | }; 211 | } 212 | 213 | render() { 214 | return ( 215 | 216 | ); 217 | } 218 | } 219 | 220 | YourApp.childContextTypes = { 221 | aboveTheFoldOnlyServerRender: React.PropTypes.shape({ 222 | AnotherComponent: React.PropTypes.bool 223 | }) 224 | }; 225 | ``` 226 | 227 | To learn more about this essential stand alone module visit the `above-the-fold-only-server-render` [Github repo](https://github.com/electrode-io/above-the-fold-only-server-render). 228 | 229 | Built with :heart: by [Team Electrode](https://github.com/orgs/electrode-io/people) @WalmartLabs. 230 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-universal-hot-example", 3 | "description": "Example of an isomorphic (universal) webapp using react redux and hot reloading", 4 | "repository": "https://github.com/erikras/react-redux-universal-hot-example", 5 | "logo": "http://node-js-sample.herokuapp.com/node.svg", 6 | "keywords": [ 7 | "react", 8 | "isomorphic", 9 | "universal", 10 | "webpack", 11 | "express", 12 | "hot reloading", 13 | "react-hot-reloader", 14 | "redux", 15 | "starter", 16 | "boilerplate", 17 | "babel" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../server.babel'); // babel registration (runtime transpilation for node) 3 | var path = require('path'); 4 | var rootDir = path.resolve(__dirname, '..'); 5 | /** 6 | * Define isomorphic constants. 7 | */ 8 | global.__CLIENT__ = false; 9 | global.__SERVER__ = true; 10 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 11 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; 12 | 13 | if (__DEVELOPMENT__) { 14 | if (!require('piping')({ 15 | hook: true, 16 | ignore: /(\/\.|~$|\.json|\.scss$)/i 17 | })) { 18 | return; 19 | } 20 | } 21 | 22 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 23 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 24 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools')) 25 | .development(__DEVELOPMENT__) 26 | .server(rootDir, function() { 27 | require('../src/server'); 28 | }); 29 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.0 4 | environment: 5 | CONTINUOUS_INTEGRATION: true 6 | 7 | dependencies: 8 | cache_directories: 9 | - node_modules 10 | override: 11 | - npm prune && npm install 12 | 13 | test: 14 | override: 15 | - npm run lint 16 | - npm test 17 | - npm run test-node 18 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | browsers: ['PhantomJS'], 7 | 8 | singleRun: !!process.env.CI, 9 | 10 | frameworks: [ 'mocha' ], 11 | 12 | files: [ 13 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 14 | 'tests.webpack.js' 15 | ], 16 | 17 | preprocessors: { 18 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] 19 | }, 20 | 21 | reporters: [ 'mocha' ], 22 | 23 | plugins: [ 24 | require("karma-webpack"), 25 | require("karma-mocha"), 26 | require("karma-mocha-reporter"), 27 | require("karma-phantomjs-launcher"), 28 | require("karma-sourcemap-loader") 29 | ], 30 | 31 | webpack: { 32 | devtool: 'inline-source-map', 33 | module: { 34 | loaders: [ 35 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} }, 36 | { test: /\.js$/, exclude: /node_modules/, loaders: ['babel']}, 37 | { test: /\.json$/, loader: 'json-loader' }, 38 | { test: /\.less$/, loader: 'style!css!less' }, 39 | { test: /\.scss$/, loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' } 40 | ] 41 | }, 42 | resolve: { 43 | modulesDirectories: [ 44 | 'src', 45 | 'node_modules' 46 | ], 47 | extensions: ['', '.json', '.js'] 48 | }, 49 | plugins: [ 50 | new webpack.IgnorePlugin(/\.json$/), 51 | new webpack.NoErrorsPlugin(), 52 | new webpack.DefinePlugin({ 53 | __CLIENT__: true, 54 | __SERVER__: false, 55 | __DEVELOPMENT__: true, 56 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE 57 | }) 58 | ] 59 | }, 60 | 61 | webpackServer: { 62 | noInfo: true 63 | } 64 | 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-universal-hot-example", 3 | "description": "Example of an isomorphic (universal) webapp using react redux and hot reloading", 4 | "author": "Erik Rasmussen (http://github.com/erikras)", 5 | "license": "MIT", 6 | "version": "0.9.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/erikras/react-redux-universal-hot-example" 10 | }, 11 | "homepage": "https://github.com/erikras/react-redux-universal-hot-example", 12 | "keywords": [ 13 | "react", 14 | "isomorphic", 15 | "universal", 16 | "webpack", 17 | "express", 18 | "hot reloading", 19 | "react-hot-reloader", 20 | "redux", 21 | "starter", 22 | "boilerplate", 23 | "babel" 24 | ], 25 | "main": "bin/server.js", 26 | "scripts": { 27 | "start": "concurrent --kill-others \"npm run start-prod\"", 28 | "start-prod": "better-npm-run start-prod", 29 | "start-prod-api": "better-npm-run start-prod-api", 30 | "build": "better-npm-run build", 31 | "postinstall": "npm run build", 32 | "lint": "eslint -c .eslintrc src api", 33 | "start-dev": "better-npm-run start-dev", 34 | "start-dev-api": "better-npm-run start-dev-api", 35 | "watch-client": "better-npm-run watch-client", 36 | "dev": "concurrent --kill-others \"npm run watch-client\" \"npm run start-dev\" \"npm run start-dev-api\"", 37 | "test": "karma start", 38 | "test-node": "./node_modules/mocha/bin/mocha $(find api -name '*-test.js') --compilers js:babel-core/register", 39 | "test-node-watch": "./node_modules/mocha/bin/mocha $(find api -name '*-test.js') --compilers js:babel-core/register --watch" 40 | }, 41 | "betterScripts": { 42 | "start-prod": { 43 | "command": "node ./bin/server.js", 44 | "env": { 45 | "NODE_PATH": "./src", 46 | "NODE_ENV": "production", 47 | "PORT": 8080, 48 | "APIPORT": 3030 49 | } 50 | }, 51 | "start-prod-api": { 52 | "command": "node ./bin/api.js", 53 | "env": { 54 | "NODE_PATH": "./api", 55 | "NODE_ENV": "production", 56 | "APIPORT": 3030 57 | } 58 | }, 59 | "start-dev": { 60 | "command": "node ./bin/server.js", 61 | "env": { 62 | "NODE_PATH": "./src", 63 | "NODE_ENV": "development", 64 | "PORT": 3000, 65 | "APIPORT": 3030 66 | } 67 | }, 68 | "start-dev-api": { 69 | "command": "node ./bin/api.js", 70 | "env": { 71 | "NODE_PATH": "./api", 72 | "NODE_ENV": "development", 73 | "APIPORT": 3030 74 | } 75 | }, 76 | "watch-client": { 77 | "command": "node webpack/webpack-dev-server.js", 78 | "env": { 79 | "UV_THREADPOOL_SIZE": 100, 80 | "NODE_PATH": "./src", 81 | "PORT": 3000, 82 | "APIPORT": 3030 83 | } 84 | }, 85 | "build": { 86 | "command": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js", 87 | "env": { 88 | "NODE_ENV": "production" 89 | } 90 | } 91 | }, 92 | "dependencies": { 93 | "above-the-fold-only-server-render": "^1.0.2", 94 | "babel-core": "^6.5.2", 95 | "babel-loader": "^6.2.1", 96 | "babel-plugin-add-module-exports": "^0.1.2", 97 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 98 | "babel-plugin-transform-react-display-name": "^6.3.13", 99 | "babel-plugin-transform-runtime": "^6.3.13", 100 | "babel-polyfill": "^6.3.14", 101 | "babel-preset-es2015": "^6.3.13", 102 | "babel-preset-react": "^6.3.13", 103 | "babel-preset-stage-0": "^6.3.13", 104 | "babel-register": "^6.3.13", 105 | "babel-runtime": "^6.3.19", 106 | "body-parser": "^1.14.1", 107 | "compression": "^1.6.0", 108 | "electrode-react-ssr-caching": "^0.1.3", 109 | "electrode-redux-router-engine": "^1.0.0", 110 | "express": "^4.13.3", 111 | "express-session": "^1.12.1", 112 | "file-loader": "^0.8.5", 113 | "hoist-non-react-statics": "^1.0.3", 114 | "http-proxy": "^1.12.0", 115 | "invariant": "^2.2.0", 116 | "less": "^2.5.3", 117 | "less-loader": "^2.2.1", 118 | "lru-memoize": "^1.0.0", 119 | "map-props": "^1.0.0", 120 | "piping": "^0.3.0", 121 | "pretty-error": "^1.2.0", 122 | "react": "^15.3.1", 123 | "react-bootstrap": "^0.28.1", 124 | "react-dom": "^15.3.0", 125 | "react-helmet": "^2.2.0", 126 | "react-inline-css": "^2.0.0", 127 | "react-redux": "^4.0.0", 128 | "react-router": "2.8.1", 129 | "react-router-bootstrap": "^0.20.1", 130 | "react-router-redux": "^4.0.0", 131 | "redux": "^3.0.4", 132 | "redux-async-connect": "^1.0.0-rc2", 133 | "redux-form": "^3.0.12", 134 | "scroll-behavior": "^0.3.2", 135 | "serialize-javascript": "^1.1.2", 136 | "serve-favicon": "^2.3.0", 137 | "socket.io": "^1.3.7", 138 | "socket.io-client": "^1.3.7", 139 | "superagent": "^1.4.0", 140 | "url-loader": "^0.5.7", 141 | "warning": "^2.1.0", 142 | "webpack-isomorphic-tools": "^2.2.18" 143 | }, 144 | "devDependencies": { 145 | "autoprefixer-loader": "^3.1.0", 146 | "babel-eslint": "^5.0.0-beta6", 147 | "babel-plugin-react-transform": "^2.0.0", 148 | "babel-plugin-typecheck": "^3.6.0", 149 | "better-npm-run": "0.0.8", 150 | "bootstrap-sass": "^3.3.5", 151 | "bootstrap-sass-loader": "^1.0.9", 152 | "chai": "^3.3.0", 153 | "clean-webpack-plugin": "^0.1.6", 154 | "concurrently": "^0.1.1", 155 | "css-loader": "^0.23.1", 156 | "eslint": "1.10.3", 157 | "eslint-config-airbnb": "0.1.0", 158 | "eslint-loader": "^1.0.0", 159 | "eslint-plugin-import": "^0.8.0", 160 | "eslint-plugin-react": "^3.5.0", 161 | "extract-text-webpack-plugin": "^0.9.1", 162 | "font-awesome": "^4.4.0", 163 | "font-awesome-webpack": "0.0.4", 164 | "json-loader": "^0.5.4", 165 | "karma": "^0.13.10", 166 | "karma-cli": "^0.1.1", 167 | "karma-mocha": "^0.2.0", 168 | "karma-mocha-reporter": "^1.1.1", 169 | "karma-phantomjs-launcher": "^0.2.1", 170 | "karma-sourcemap-loader": "^0.3.5", 171 | "karma-webpack": "^1.7.0", 172 | "mocha": "^2.3.3", 173 | "node-sass": "^3.4.2", 174 | "phantomjs": "^1.9.18", 175 | "phantomjs-polyfill": "0.0.1", 176 | "react-a11y": "^0.2.6", 177 | "react-addons-test-utils": "^0.14.0", 178 | "react-transform-catch-errors": "^1.0.0", 179 | "react-transform-hmr": "^1.0.1", 180 | "redbox-react": "^1.1.1", 181 | "redux-devtools": "^3.0.0-beta-3", 182 | "redux-devtools-dock-monitor": "^1.0.0-beta-3", 183 | "redux-devtools-log-monitor": "^1.0.0-beta-3", 184 | "sass-loader": "^3.1.2", 185 | "sinon": "^1.17.2", 186 | "strip-loader": "^0.1.0", 187 | "style-loader": "^0.13.0", 188 | "timekeeper": "0.0.5", 189 | "webpack": "^1.12.9", 190 | "webpack-dev-middleware": "^1.4.0", 191 | "webpack-hot-middleware": "^2.5.0" 192 | }, 193 | "engines": { 194 | "node": "5.6.0" 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /server.babel.js: -------------------------------------------------------------------------------- 1 | // enable runtime transpilation to use ES6/7 in node 2 | 3 | var fs = require('fs'); 4 | 5 | var babelrc = fs.readFileSync('./.babelrc'); 6 | var config; 7 | 8 | try { 9 | config = JSON.parse(babelrc); 10 | } catch (err) { 11 | console.error('==> ERROR: Error parsing your .babelrc.'); 12 | console.error(err); 13 | } 14 | 15 | require('babel-register')(config); 16 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * THIS IS THE ENTRY POINT FOR THE CLIENT, JUST LIKE server.js IS THE ENTRY POINT FOR THE SERVER. 3 | */ 4 | import 'babel-polyfill'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { createStore } from 'redux'; 8 | import {Provider} from 'react-redux'; 9 | import { Router } from 'react-router'; 10 | import { browserHistory } from 'react-router'; 11 | import getRoutes from './routes'; 12 | 13 | const dest = document.getElementById('content'); 14 | 15 | const rootReducer = (ss) => ss; 16 | const store = createStore(rootReducer, window.__data); 17 | 18 | ReactDOM.render( 19 | 20 | {getRoutes()} 21 | , 22 | dest 23 | ); 24 | 25 | if (process.env.NODE_ENV !== 'production') { 26 | window.React = React; // enable debugger 27 | 28 | if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) { 29 | console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/SSRCachingSimpleType.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class SSRCachingSimpleTypeWrapper extends React.Component { 5 | render() { 6 | const count = this.props.count; 7 | 8 | const elements = []; 9 | 10 | for (let ii = 0; ii < count; ii++) { 11 | elements.push(); 12 | } 13 | 14 | return ( 15 |
16 | {elements} 17 |
18 | ); 19 | } 20 | } 21 | 22 | SSRCachingSimpleTypeWrapper.propTypes = { 23 | count: React.PropTypes.number 24 | }; 25 | 26 | class SSRCachingSimpleType extends React.Component { 27 | render() { 28 | return ( 29 |
30 |

{this.props.navEntry}

31 |
32 | ); 33 | } 34 | } 35 | 36 | SSRCachingSimpleType.propTypes = { 37 | navEntry: React.PropTypes.string 38 | }; 39 | 40 | const mapStateToProps = (state) => ({ 41 | count: state.count 42 | }); 43 | 44 | export default connect( 45 | mapStateToProps 46 | )(SSRCachingSimpleTypeWrapper); 47 | -------------------------------------------------------------------------------- /src/components/SSRCachingTemplateType.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class SSRCachingTemplateTypeWrapper extends React.Component { 5 | render() { 6 | const count = this.props.count; 7 | const elements = []; 8 | 9 | for (let ii = 0; ii < count; ii++) { 10 | elements.push(); 11 | } 12 | 13 | return ( 14 |
15 | { elements } 16 |
17 | ); 18 | } 19 | } 20 | 21 | SSRCachingTemplateTypeWrapper.propTypes = { 22 | count: React.PropTypes.number 23 | }; 24 | 25 | class SSRCachingTemplateType extends React.Component { 26 | render() { 27 | return ( 28 |
29 |

{this.props.name} and {this.props.title} and {this.props.rating}

30 |
31 | ); 32 | } 33 | } 34 | 35 | SSRCachingTemplateType.propTypes = { 36 | name: React.PropTypes.string, 37 | title: React.PropTypes.string, 38 | rating: React.PropTypes.string 39 | }; 40 | 41 | const mapStateToProps = (state) => ({ 42 | count: state.count 43 | }); 44 | 45 | export default connect( 46 | mapStateToProps 47 | )(SSRCachingTemplateTypeWrapper); 48 | -------------------------------------------------------------------------------- /src/components/above-the-fold.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AboveTheFoldOnlyServerRender} from 'above-the-fold-only-server-render'; 3 | 4 | export class AboveFold extends React.Component { 5 | 6 | render() { 7 | return ( 8 |
9 |

Above-the-fold-only-server-render: Increase Your Performance

10 | 11 |
12 |

This will skip server rendering if the 'AboveTheFoldOnlyServerRender' 13 | lines are present, or uncommented out.

14 |

This will be rendered on the server and visible if the 'AboveTheFoldOnlyServerRender' 15 | lines are commented out.

16 |

Try manually toggling this component to see it in action

17 |

18 | Read more about this module and see our live demo 20 | 21 |

22 |
23 |
24 |

This is below the 'Above the fold closing tag'

25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class Home extends React.Component { 4 | render() { 5 | return ( 6 |
7 |

Hello Electrode

8 |

Demonstration Components

9 |
    10 |
  • 11 | 12 | Above the Fold Render 13 | 14 |

    Increase your App's performance by using a skip prop

    15 |
  • 16 |
  • 17 | 18 | SSR Caching - Simple 19 | 20 |

    Component Props become the cache key. This is useful for cases like Header and Footer where the number of variations of props data is minimal which will make sure the cache size stays small.

    21 |
  • 22 |
  • 23 | 24 | SSR Caching- Template Type 25 | 26 |

    Components Props are first tokenized and then the generated template html is cached. The idea is akin to generating logic-less handlebars template from your React components and then use string replace to process the template with different props. This is useful for cases like displaying Product information in a Carousel where you have millions of products in the repository.

    27 |
  • 28 |
29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill'); 2 | 3 | const environment = { 4 | development: { 5 | isProduction: false 6 | }, 7 | production: { 8 | isProduction: true 9 | } 10 | }[process.env.NODE_ENV || 'development']; 11 | 12 | module.exports = Object.assign({ 13 | host: process.env.HOST || 'localhost', 14 | port: process.env.PORT, 15 | apiHost: process.env.APIHOST || 'localhost', 16 | apiPort: process.env.APIPORT, 17 | app: { 18 | title: 'React Redux Example', 19 | description: 'All the modern best practices in one example.', 20 | head: { 21 | titleTemplate: 'React Redux Example: %s', 22 | meta: [ 23 | {name: 'description', content: 'All the modern best practices in one example.'}, 24 | {charset: 'utf-8'}, 25 | {property: 'og:site_name', content: 'React Redux Example'}, 26 | {property: 'og:image', content: 'https://react-redux.herokuapp.com/logo.jpg'}, 27 | {property: 'og:locale', content: 'en_US'}, 28 | {property: 'og:title', content: 'React Redux Example'}, 29 | {property: 'og:description', content: 'All the modern best practices in one example.'}, 30 | {property: 'og:card', content: 'summary'}, 31 | {property: 'og:site', content: '@erikras'}, 32 | {property: 'og:creator', content: '@erikras'}, 33 | {property: 'og:image:width', content: '200'}, 34 | {property: 'og:image:height', content: '200'} 35 | ] 36 | } 37 | }, 38 | 39 | }, environment); 40 | -------------------------------------------------------------------------------- /src/helpers/Html.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import ReactDOM from 'react-dom/server'; // eslint-disable-line no-unused-vars 3 | import serialize from 'serialize-javascript'; 4 | import Helmet from 'react-helmet'; 5 | 6 | /** 7 | * Wrapper component containing HTML metadata and boilerplate tags. 8 | * Used in server-side code only to wrap the string output of the 9 | * rendered route component. 10 | * 11 | * The only thing this component doesn't (and can't) include is the 12 | * HTML doctype declaration, which is added to the rendered output 13 | * by the server.js file. 14 | */ 15 | export default class Html extends Component { 16 | static propTypes = { 17 | assets: PropTypes.object, 18 | content: PropTypes.string, 19 | store: PropTypes.object 20 | }; 21 | 22 | render() { 23 | const {assets, content, store} = this.props; 24 | const head = Helmet.rewind(); 25 | 26 | return ( 27 | 28 | 29 | {head.base.toComponent()} 30 | {head.title.toComponent()} 31 | {head.meta.toComponent()} 32 | {head.link.toComponent()} 33 | {head.script.toComponent()} 34 | 35 | 36 | 37 | {/* styles (will be present only in production with webpack extract text plugin) */} 38 | {Object.keys(assets.styles).map((style, key) => 39 | 41 | )} 42 | 43 | {/* (will be present only in development mode) */} 44 | {/* outputs a