├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── nodemon.json ├── package.json ├── src ├── client │ ├── main.js │ └── routes.js ├── server │ ├── app.js │ └── routes.js └── shared │ ├── boardgames.json │ ├── cache.js │ ├── components │ ├── app.jsx │ ├── card.jsx │ ├── dataWrapper.jsx │ ├── detail.jsx │ └── home.jsx │ ├── routeUtils.js │ ├── routes.js │ ├── store.js │ └── viewUtils.js ├── views └── pages │ └── index.ejs └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Elyse Kolker Gordon 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iso-react-demo 2 | Example of how to build an isomorphic react app. 3 | [Slides from #strangeloop](https://speakerdeck.com/elyseko/building-isomorphic-web-apps-with-react) 4 | 5 | Runs at localhost:4000. 6 | 7 | # Setup 8 | ``` 9 | npm install 10 | 11 | # if you don't have webpack 12 | npm install webpack -g 13 | ``` 14 | 15 | Run the following in separate windows. 16 | ``` 17 | webpack --watch 18 | gulp browser-sync 19 | npm start 20 | ``` 21 | 22 | * When you start browser sync it will open a window for you automatically, 23 | since there is no index.html it will show an error. To verify it is working, test 24 | /build/browser.js 25 | 26 | # Libraries 27 | * Requires webpack to build 28 | * Browser sync is to server static assets so we don't need to bundle them 29 | into the webpack bundle for the server. Browser sync will decide what port to 30 | use automatically and log it in the terminal window. 31 | 32 | # References/Resources 33 | * [React](https://facebook.github.io/react/) 34 | * [React Router](https://github.com/rackt/react-router) 35 | * [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html) 36 | * [Webpack](https://webpack.github.io/) 37 | * [Browser Sync](http://www.browsersync.io/) 38 | 39 | * [List of Isomorphic Apps](http://isomorphic.net/) 40 | * [React Nexus](https://blog.rotenberg.io/isomorphic-apps-done-right-with-react-nexus/) 41 | * [Pellet (Vevo)](https://github.com/Rebelizer/pellet) 42 | 43 | * [airbnb isomorphic article](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) 44 | * [Webpack React How To](http://www.christianalfoni.com/articles/2015_04_19_The-ultimate-webpack-setup) 45 | * [Webpack How TO](https://github.com/petehunt/webpack-howto) 46 | * [Webpack Dev Server Help](http://stackoverflow.com/questions/27532246/how-to-use-webpack-for-development-without-webpack-dev-server) 47 | * [Exploring Isomorphic Javascript](http://nicolashery.com/exploring-isomorphic-javascript/) 48 | * [Simple Isomrophic Example](http://jmfurlott.com/tutorial-setting-up-a-simple-isomorphic-react-app/) 49 | * [Getting Started with React](https://blog.risingstack.com/the-react-way-getting-started-tutorial/) 50 | * [Webpack Env Variables](http://nicolashery.com/using-environment-variables-with-webpack-and-divshot/) 51 | * [Higher Order Components](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775) 52 | * [Node and Webpack](http://jlongster.com/Backend-Apps-with-Webpack--Part-II) 53 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var path = require('path'); 3 | var browserSync = require('browser-sync').create(); 4 | 5 | gulp.task('browser-sync', function () { 6 | browserSync.init({ 7 | server: path.join(__dirname, "public/") 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "ignore": ["*_spec.js", "node_modules/*"] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iso-react-demo", 3 | "version": "0.0.0", 4 | "description": "Demo of isomorphic react app", 5 | "main": "src/server/app.js", 6 | "scripts": { 7 | "start": "./node_modules/.bin/nodemon public/build/server.js", 8 | "debug": "node-debug public/build/server.js", 9 | "test": "./node_modules/.bin/jasmine-node test --autoTest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://elyseko@github.com/elyseko/iso-react-demo.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "isomorphic" 18 | ], 19 | "author": "Elyse Kolker Gordon", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/elyseko/iso-react-demo/issues" 23 | }, 24 | "homepage": "https://github.com/elyseko/iso-react-demo", 25 | "dependencies": { 26 | "babel-core": "^5.8.22", 27 | "babel-loader": "^5.3.2", 28 | "babel-plugin-dev-expression": "^0.1.0", 29 | "babel-runtime": "^5.8.20", 30 | "deep-equal": "^1.0.1", 31 | "ejs": "^2.3.3", 32 | "ejs-loader": "^0.2.1", 33 | "express": "^4.13.3", 34 | "history": "^1.9.0", 35 | "http-proxy": "^1.11.1", 36 | "invariant": "^2.1.0", 37 | "json-loader": "^0.5.2", 38 | "react": "^0.14.0-beta3", 39 | "react-dom": "^0.14.0-beta3", 40 | "react-router": "^1.0.0-beta4", 41 | "style-loader": "^0.12.3", 42 | "stylus-loader": "^1.2.1", 43 | "superagent": "^1.3.0", 44 | "warning": "^2.0.0", 45 | "webpack": "^1.11.0" 46 | }, 47 | "devDependencies": { 48 | "browser-sync": "^2.9.6", 49 | "gulp": "^3.9.0", 50 | "gulp-babel": "^5.2.1", 51 | "nodemon": "^1.7.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/client/main.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import React from 'react' 3 | import routes from './routes' 4 | import Cache from '../shared/cache' 5 | 6 | let cache = new Cache(window.$data()); 7 | 8 | ReactDOM.render(routes, 9 | document.getElementById('react') 10 | ) 11 | -------------------------------------------------------------------------------- /src/client/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Client (browser) Routes 3 | 4 | - includes shared routes 5 | - sets up history in browsery compliant way 6 | */ 7 | 8 | import React from "react" 9 | import { Router, IndexRoute } from "react-router" 10 | import createHistory from '$history' 11 | import sharedRoutes from "../shared/routes" 12 | import DataWrapper from '../shared/components/dataWrapper' 13 | 14 | 15 | // runs once for each component that the router 16 | // knows about 17 | function createElement(Component, props) { 18 | return 19 | } 20 | 21 | export default ( 22 | 23 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from 'path'; 3 | 4 | // instantiate react-router 5 | import router from './routes' 6 | 7 | const app = express(); 8 | 9 | // point at the ejs templates 10 | app.set('view engine', 'ejs'); 11 | 12 | //view routes 13 | app.get('/*', router) 14 | 15 | app.listen(4000, function(){ 16 | console.log("running on port 4000") 17 | }); 18 | -------------------------------------------------------------------------------- /src/server/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/server'; 3 | 4 | import { RoutingContext, match } from 'react-router' 5 | import routes from '../shared/routes'; 6 | import createLocation from '$history' 7 | import routeUtils from '../shared/routeUtils' 8 | 9 | let createElement = (Component, props) => { 10 | props.data = props.params.data; 11 | props.params.data = undefined; 12 | return 13 | } 14 | 15 | module.exports = (req, res) => { 16 | let location = createLocation(req.url); 17 | match({ routes, location }, (error, redirectLocation, renderProps) => { 18 | if (redirectLocation) 19 | res.redirect(301, redirectLocation.pathname + redirectLocation.search) 20 | else if (error) 21 | res.status(500).send(error.message) 22 | else if (renderProps == null) 23 | res.status(404).send('Not found') 24 | else { 25 | let components = renderProps.components 26 | let requests = routeUtils.getListOfRequests(components); 27 | renderProps.createElement = createElement; 28 | // define callback 29 | let count = requests.length; 30 | let viewData = {}; 31 | let callback = (err, data)=> { 32 | if(!err) { 33 | viewData[data.id] = data.result; 34 | } else { 35 | viewData[data.id] = err; 36 | } 37 | count--; 38 | if (count <= 0) { 39 | renderProps.params.data = viewData; 40 | let html = ReactDOM.renderToString(); 41 | res.render('pages/index', {"title": "Test", "html": html, data: JSON.stringify(viewData)}); 42 | } 43 | } 44 | routeUtils.batchRequests(requests, callback, renderProps.params); 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/shared/boardgames.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "id": 1, 5 | "name": "M.U.L.E. The Board Game", 6 | "thumbnail": "//cf.geekdo-images.com/images/pic2634492_t.jpg", 7 | "bgg_id": 182619, 8 | "year_published": "2015" 9 | }, 10 | { 11 | "id": 2, 12 | "name": "Nevermore", 13 | "thumbnail": "//cf.geekdo-images.com/images/pic2404085_t.jpg", 14 | "bgg_id": 173047, 15 | "year_published": "2015" 16 | }, 17 | { 18 | "id": 3, 19 | "name": "Star Wars: Imperial Assault", 20 | "thumbnail": "//cf.geekdo-images.com/images/pic2247647_t.jpg", 21 | "bgg_id": 164153, 22 | "year_published": "2014" 23 | }, 24 | { 25 | "id": 4, 26 | "name": "Treasure Hunter", 27 | "thumbnail": "//cf.geekdo-images.com/images/pic2632365_t.jpg", 28 | "bgg_id": 182189, 29 | "year_published": "2015" 30 | }, 31 | { 32 | "id": 5, 33 | "name": "Machi Koro: Deluxe Edition", 34 | "thumbnail": "//cf.geekdo-images.com/images/pic2486977_t.jpg", 35 | "bgg_id": 175239, 36 | "year_published": "2015" 37 | }, 38 | { 39 | "id": 6, 40 | "name": "T.I.M.E Stories", 41 | "thumbnail": "//cf.geekdo-images.com/images/pic2617634_t.png", 42 | "bgg_id": 146508, 43 | "year_published": "2015" 44 | }, 45 | { 46 | "id": 7, 47 | "name": "Churchill", 48 | "thumbnail": "//cf.geekdo-images.com/images/pic2467234_t.jpg", 49 | "bgg_id": 132018, 50 | "year_published": "2015" 51 | }, 52 | { 53 | "id": 8, 54 | "name": "Forbidden Stars", 55 | "thumbnail": "//cf.geekdo-images.com/images/pic2471359_t.jpg", 56 | "bgg_id": 175155, 57 | "year_published": "2015" 58 | }, 59 | { 60 | "id": 9, 61 | "name": "Star Trek: Frontiers", 62 | "thumbnail": "//cf.geekdo-images.com/images/pic2627040_t.jpg", 63 | "bgg_id": 182340, 64 | "year_published": "2016" 65 | }, 66 | { 67 | "id": 10, 68 | "name": "Glory to Rome", 69 | "thumbnail": "//cf.geekdo-images.com/images/pic1079512_t.png", 70 | "bgg_id": 19857, 71 | "year_published": "2005" 72 | }, 73 | { 74 | "id": 11, 75 | "name": "Blood Rage", 76 | "thumbnail": "//cf.geekdo-images.com/images/pic2439223_t.jpg", 77 | "bgg_id": 170216, 78 | "year_published": "2015" 79 | }, 80 | { 81 | "id": 12, 82 | "name": "Cultists of Cthulhu", 83 | "thumbnail": "//cf.geekdo-images.com/images/pic2628734_t.png", 84 | "bgg_id": 157452, 85 | "year_published": "2016" 86 | }, 87 | { 88 | "id": 13, 89 | "name": "Ashes: Rise of the Phoenixborn", 90 | "thumbnail": "//cf.geekdo-images.com/images/pic2479679_t.jpg", 91 | "bgg_id": 167400, 92 | "year_published": "2015" 93 | }, 94 | { 95 | "id": 14, 96 | "name": "City of Iron", 97 | "thumbnail": "//cf.geekdo-images.com/images/pic1292441_t.jpg", 98 | "bgg_id": 123499, 99 | "year_published": "2013" 100 | }, 101 | { 102 | "id": 15, 103 | "name": "Legendary: A Marvel Deck Building Game", 104 | "thumbnail": "//cf.geekdo-images.com/images/pic1430769_t.jpg", 105 | "bgg_id": 129437, 106 | "year_published": "2012" 107 | }, 108 | { 109 | "id": 16, 110 | "name": "Twilight Struggle", 111 | "thumbnail": "//cf.geekdo-images.com/images/pic361592_t.jpg", 112 | "bgg_id": 12333, 113 | "year_published": "2005" 114 | }, 115 | { 116 | "id": 17, 117 | "name": "Codenames", 118 | "thumbnail": "//cf.geekdo-images.com/images/pic2582929_t.jpg", 119 | "bgg_id": 178900, 120 | "year_published": "2015" 121 | }, 122 | { 123 | "id": 18, 124 | "name": "Dead of Winter: A Crossroads Game", 125 | "thumbnail": "//cf.geekdo-images.com/images/pic2221472_t.jpg", 126 | "bgg_id": 150376, 127 | "year_published": "2014" 128 | }, 129 | { 130 | "id": 19, 131 | "name": "Star Wars: Imperial Assault – Twin Shadows", 132 | "thumbnail": "//cf.geekdo-images.com/images/pic2473289_t.jpg", 133 | "bgg_id": 175211, 134 | "year_published": "2015" 135 | }, 136 | { 137 | "id": 20, 138 | "name": "Star Wars: X-Wing Miniatures Game", 139 | "thumbnail": "//cf.geekdo-images.com/images/pic1603292_t.jpg", 140 | "bgg_id": 103885, 141 | "year_published": "2012" 142 | }, 143 | { 144 | "id": 21, 145 | "name": "Mage Knight Board Game", 146 | "thumbnail": "//cf.geekdo-images.com/images/pic1083380_t.jpg", 147 | "bgg_id": 96848, 148 | "year_published": "2011" 149 | }, 150 | { 151 | "id": 22, 152 | "name": "Android: Netrunner", 153 | "thumbnail": "//cf.geekdo-images.com/images/pic1324609_t.jpg", 154 | "bgg_id": 124742, 155 | "year_published": "2012" 156 | }, 157 | { 158 | "id": 23, 159 | "name": "Legendary: Secret Wars - Volume 1", 160 | "thumbnail": "//cf.geekdo-images.com/images/pic2545261_t.png", 161 | "bgg_id": 175156, 162 | "year_published": "2015" 163 | }, 164 | { 165 | "id": 24, 166 | "name": "La Granja", 167 | "thumbnail": "//cf.geekdo-images.com/images/pic2031777_t.jpg", 168 | "bgg_id": 146886, 169 | "year_published": "2014" 170 | }, 171 | { 172 | "id": 25, 173 | "name": "Eldritch Horror", 174 | "thumbnail": "//cf.geekdo-images.com/images/pic1872452_t.jpg", 175 | "bgg_id": 146021, 176 | "year_published": "2013" 177 | }, 178 | { 179 | "id": 26, 180 | "name": "Terra Mystica", 181 | "thumbnail": "//cf.geekdo-images.com/images/pic1356616_t.jpg", 182 | "bgg_id": 120677, 183 | "year_published": "2012" 184 | }, 185 | { 186 | "id": 27, 187 | "name": "CVlizations", 188 | "thumbnail": "//cf.geekdo-images.com/images/pic2622816_t.jpg", 189 | "bgg_id": 181494, 190 | "year_published": "2015" 191 | }, 192 | { 193 | "id": 28, 194 | "name": "Mission: Red Planet (Second Edition)", 195 | "thumbnail": "//cf.geekdo-images.com/images/pic2499748_t.jpg", 196 | "bgg_id": 176920, 197 | "year_published": "2015" 198 | }, 199 | { 200 | "id": 29, 201 | "name": "Mistfall", 202 | "thumbnail": "//cf.geekdo-images.com/images/pic2410035_t.png", 203 | "bgg_id": 168274, 204 | "year_published": "2015" 205 | }, 206 | { 207 | "id": 30, 208 | "name": "Mice and Mystics", 209 | "thumbnail": "//cf.geekdo-images.com/images/pic1312072_t.jpg", 210 | "bgg_id": 124708, 211 | "year_published": "2012" 212 | }, 213 | { 214 | "id": 31, 215 | "name": "Evolution", 216 | "thumbnail": "//cf.geekdo-images.com/images/pic2558560_t.jpg", 217 | "bgg_id": 155703, 218 | "year_published": "2014" 219 | }, 220 | { 221 | "id": 32, 222 | "name": "The Voyages of Marco Polo", 223 | "thumbnail": "//cf.geekdo-images.com/images/pic2461346_t.png", 224 | "bgg_id": 171623, 225 | "year_published": "2015" 226 | }, 227 | { 228 | "id": 33, 229 | "name": "The Lord of the Rings: The Card Game", 230 | "thumbnail": "//cf.geekdo-images.com/images/pic906495_t.jpg", 231 | "bgg_id": 77423, 232 | "year_published": "2011" 233 | }, 234 | { 235 | "id": 34, 236 | "name": "Scythe", 237 | "thumbnail": "//cf.geekdo-images.com/images/pic2323719_t.jpg", 238 | "bgg_id": 169786, 239 | "year_published": "2016" 240 | }, 241 | { 242 | "id": 35, 243 | "name": "Imperial Settlers", 244 | "thumbnail": "//cf.geekdo-images.com/images/pic2000680_t.jpg", 245 | "bgg_id": 154203, 246 | "year_published": "2014" 247 | }, 248 | { 249 | "id": 36, 250 | "name": "Caverna: The Cave Farmers", 251 | "thumbnail": "//cf.geekdo-images.com/images/pic1790789_t.jpg", 252 | "bgg_id": 102794, 253 | "year_published": "2013" 254 | }, 255 | { 256 | "id": 37, 257 | "name": "Viceroy", 258 | "thumbnail": "//cf.geekdo-images.com/images/pic2254354_t.jpg", 259 | "bgg_id": 157526, 260 | "year_published": "2014" 261 | }, 262 | { 263 | "id": 38, 264 | "name": "504", 265 | "thumbnail": "//cf.geekdo-images.com/images/pic2570515_t.jpg", 266 | "bgg_id": 175878, 267 | "year_published": "2015" 268 | }, 269 | { 270 | "id": 39, 271 | "name": "Warhammer Quest: The Adventure Card Game", 272 | "thumbnail": "//cf.geekdo-images.com/images/pic2625794_t.jpg", 273 | "bgg_id": 181521, 274 | "year_published": "2015" 275 | }, 276 | { 277 | "id": 40, 278 | "name": "Flick 'em Up!", 279 | "thumbnail": "//cf.geekdo-images.com/images/pic2439671_t.png", 280 | "bgg_id": 169124, 281 | "year_published": "2015" 282 | }, 283 | { 284 | "id": 41, 285 | "name": "Shadows of Brimstone: City of the Ancients", 286 | "thumbnail": "//cf.geekdo-images.com/images/pic2037825_t.jpg", 287 | "bgg_id": 146791, 288 | "year_published": "2014" 289 | }, 290 | { 291 | "id": 42, 292 | "name": "Discoveries", 293 | "thumbnail": "//cf.geekdo-images.com/images/pic2571301_t.jpg", 294 | "bgg_id": 171669, 295 | "year_published": "2015" 296 | }, 297 | { 298 | "id": 43, 299 | "name": "Mysterium", 300 | "thumbnail": "//cf.geekdo-images.com/images/pic2601683_t.jpg", 301 | "bgg_id": 181304, 302 | "year_published": "2015" 303 | }, 304 | { 305 | "id": 44, 306 | "name": "Brass", 307 | "thumbnail": "//cf.geekdo-images.com/images/pic261878_t.jpg", 308 | "bgg_id": 28720, 309 | "year_published": "2007" 310 | }, 311 | { 312 | "id": 45, 313 | "name": "Vs. System 2PCG", 314 | "thumbnail": "//cf.geekdo-images.com/images/pic2630832_t.jpg", 315 | "bgg_id": 178892, 316 | "year_published": "2015" 317 | }, 318 | { 319 | "id": 46, 320 | "name": "New York 1901", 321 | "thumbnail": "//cf.geekdo-images.com/images/pic2515532_t.jpg", 322 | "bgg_id": 174660, 323 | "year_published": "2015" 324 | }, 325 | { 326 | "id": 47, 327 | "name": "Rum & Bones", 328 | "thumbnail": "//cf.geekdo-images.com/images/pic2299556_t.jpg", 329 | "bgg_id": 168788, 330 | "year_published": "2015" 331 | }, 332 | { 333 | "id": 48, 334 | "name": "Shadowrun: Crossfire", 335 | "thumbnail": "//cf.geekdo-images.com/images/pic2060466_t.jpg", 336 | "bgg_id": 135382, 337 | "year_published": "2014" 338 | }, 339 | { 340 | "id": 49, 341 | "name": "Roll for the Galaxy", 342 | "thumbnail": "//cf.geekdo-images.com/images/pic1473629_t.jpg", 343 | "bgg_id": 132531, 344 | "year_published": "2014" 345 | }, 346 | { 347 | "id": 50, 348 | "name": "Robinson Crusoe: Adventures on the Cursed Island", 349 | "thumbnail": "//cf.geekdo-images.com/images/pic1715554_t.jpg", 350 | "bgg_id": 121921, 351 | "year_published": "2012" 352 | } 353 | ] 354 | } 355 | -------------------------------------------------------------------------------- /src/shared/cache.js: -------------------------------------------------------------------------------- 1 | const cache = {}; 2 | 3 | export default class Cache { 4 | constructor(data) { 5 | this.cache = cache; 6 | this.build(data); 7 | } 8 | 9 | add(key, data) { 10 | this.cache[key] = data; 11 | } 12 | 13 | delete(key) { 14 | delete this.cache[key]; 15 | } 16 | 17 | get(key) { 18 | return this.cache[key]; 19 | } 20 | 21 | exists(key) { 22 | return !! this.cache[key]; 23 | } 24 | 25 | build(data) { 26 | if (data) { 27 | Object.keys(data).forEach( (item, index) => { 28 | this.add(item, data[item]); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/components/app.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | Parent react component 3 | 4 | Header, footer and other components common to all 5 | routes go in here 6 | */ 7 | 8 | import React from 'react' 9 | import { Link } from "react-router" 10 | 11 | export default class App extends React.Component { 12 | 13 | render() { 14 | return ( 15 |
16 |

17 | Isomorphic React Example 18 |

19 |
20 | {this.props.children} 21 |
22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/components/card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from "react-router" 3 | 4 | export default class Card extends React.Component { 5 | render() { 6 | return ( 7 | 8 | 9 |
{this.props.name}
10 |
11 | Published in {this.props.year_published} 12 |
13 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/components/dataWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Store from '../store' 3 | import routeUtils from '../routeUtils' 4 | 5 | const store = new Store(); 6 | 7 | export default class DataWrapper extends React.Component { 8 | 9 | constructor(props) { 10 | super(); 11 | this.state = {data:{}}; 12 | this.getData(props); 13 | } 14 | 15 | componentWillReceiveProps(nextProps) { 16 | this.getData(nextProps); 17 | } 18 | 19 | getData(props) { 20 | let requests = routeUtils.getListOfRequests([props.component]) 21 | let newData = {} 22 | let count = requests.length; 23 | if (count) { 24 | requests.forEach((item, index)=> { 25 | // check item in case a component implements requestData 26 | // but does not return an item 27 | if (item) { 28 | let options = {}; 29 | let key = "" 30 | if (item.hasOwnProperty("params")) { 31 | Object.keys(item.params).forEach((option, index) => { 32 | key += options[option] = props.params[option]; 33 | }); 34 | } 35 | this.updateState = this.updateState.bind(this) 36 | //TODO: handle cleanup in component will mount 37 | store[item.request](this.updateState, item.request, options); 38 | } else { 39 | console.error("static method requestData must return a valid store request") 40 | } 41 | }); 42 | } 43 | } 44 | 45 | updateState(err, item) { 46 | let obj = this.state.data || {} 47 | obj[item.id] = item.result 48 | if (this.props) { 49 | this.setState({data: obj}) 50 | } else { 51 | this.state.data = obj 52 | } 53 | } 54 | 55 | render() { 56 | return ( 57 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/shared/components/detail.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import viewUtils from '../viewUtils' 3 | 4 | let GET_CARD = "getCard"; 5 | let GET_RELATED = "getRelated"; 6 | 7 | export default class Detail extends React.Component { 8 | 9 | static requestData() { 10 | return [{request: GET_CARD, params: {id: ":id"}}, {request: GET_RELATED, params: {id: ":id"}}]; 11 | } 12 | 13 | render() { 14 | // TODO: check for error and/or loading states 15 | let item = this.props.data[GET_CARD + this.props.params.id]; 16 | let related = this.props.data[GET_RELATED + this.props.params.id]; 17 | if (item) { 18 | return ( 19 |
20 |
21 |
22 |

{item.name}

23 | 24 |
25 |
26 |
27 | {viewUtils.renderCards(related)} 28 |
29 |
30 | ) 31 | } else { 32 | return ( 33 |
34 | Loading 35 |
36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/shared/components/home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from "react-router" 3 | import viewUtils from "../viewUtils" 4 | 5 | export default class Home extends React.Component { 6 | 7 | constructor(props) { 8 | super(); 9 | } 10 | 11 | static requestData() { 12 | return [{request: "getCards"}]; 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 |
19 | Default 20 | Reverse 21 |
22 |
23 | {viewUtils.renderCards(this.props.data.getCards)} 24 |
25 |
26 | 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/routeUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Some utility functions that are used by both the server and the browser 3 | routing logic 4 | */ 5 | 6 | import Store from './store' 7 | const store = new Store(); 8 | 9 | module.exports = { 10 | /* 11 | compiles a list of requests from the static method 12 | requestData 13 | */ 14 | getListOfRequests: function(components) { 15 | let requests = []; 16 | components.forEach((item, index) => { 17 | // make sure this is really a component 18 | if (!item) return; 19 | // check for static method on parent components 20 | if (item.hasOwnProperty("requestData")) { 21 | requests = requests.concat(item.requestData()); 22 | } 23 | }); 24 | return requests; 25 | }, 26 | /* 27 | makes a batch of calls to the store 28 | */ 29 | batchRequests: function(requests, callback, params) { 30 | requests.forEach((item, index)=> { 31 | // check item in case a component implements requestData 32 | // but does not return an item 33 | if (item) { 34 | let options = {}; 35 | if (item.hasOwnProperty("params")) { 36 | Object.keys(item.params).forEach((option, index) => { 37 | options[option] = params[option]; 38 | }); 39 | } 40 | store[item.request](callback, item.request, options); 41 | } else { 42 | console.error("static method requestData must return a valid store request") 43 | } 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/shared/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Shared routes to be used on the server and in the browser 3 | 4 | - Must be included into an instance of react-router 5 | */ 6 | 7 | import React from "react"; 8 | import { Route, IndexRoute } from "react-router"; 9 | import App from "./components/App"; 10 | import Home from "./components/Home"; 11 | import Detail from "./components/Detail"; 12 | 13 | // console.log("history", createHistory) 14 | 15 | export default ( 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/shared/store.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sample API module 3 | 4 | - Keeping it simple using callbacks, could be done with promises instead 5 | - Handles pushing data into the cache when calls are made 6 | */ 7 | 8 | import boardgames from './boardgames.json' 9 | import Cache from './cache' 10 | let cache = new Cache(); //TODO should we instantiate a better way? 11 | 12 | //stubbed data 13 | export default class Store { 14 | 15 | // fake a get call 16 | _get(callback, data, id) { 17 | let err = null; //placeholder for real calls 18 | //call API - for this example fake an async call with setTimeout 19 | setTimeout( () => { 20 | //set the response in the cache - cache exists across 21 | //multiple calls and this solves a problem with having 22 | //to make a state existence check in componentWillMount 23 | let dataToCache = data; 24 | if (err) { 25 | dataToCache = err; 26 | } 27 | cache.add(id, dataToCache); 28 | return callback(err, {result: dataToCache, id: id}); 29 | }, 200); 30 | } 31 | 32 | _checkCache(id) { 33 | //check cache to see if ids exist 34 | if (cache.exists(id)) { 35 | console.info("LOG: cache hit"); 36 | return true; 37 | } 38 | } 39 | 40 | getCards(callback, cardsId, options) { 41 | let err = null; 42 | if (this._checkCache(cardsId)) { 43 | callback(err, {result: cache.get(cardsId), id: cardsId}); 44 | } else { 45 | this._get(callback, boardgames.items, cardsId); 46 | } 47 | } 48 | 49 | getCard(callback, cardId, options) { 50 | let err = null; 51 | 52 | if (!options || !options.id) { 53 | err = {err: "missing options or no id on request"} 54 | return callback(err, {id: cardId}); 55 | } 56 | 57 | let cacheId = cardId + options.id; 58 | if (this._checkCache(cacheId)) { 59 | callback(err, {result: cache.get(cacheId), id: cacheId}); 60 | } else { 61 | this._get(callback, boardgames.items[options.id-1], cacheId); 62 | } 63 | } 64 | 65 | getRelated(callback, relatedId, options) { 66 | let err = null; 67 | 68 | if (!options || !options.id) { 69 | err = {err: "missing options or no id on request"} 70 | return callback(err, {id: cardId}); 71 | } 72 | 73 | let cacheId = relatedId + options.id; 74 | if (this._checkCache(cacheId)) { 75 | return callback(err, {result: cache.get(cacheId), id: cacheId}); 76 | } else { 77 | //TODO: hook up to actual related ap 78 | this._get(callback, boardgames.items.slice(0,4), cacheId); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/shared/viewUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Some utility functions that are used by multiple react components 3 | */ 4 | import React from 'react' 5 | import Card from './components/card' 6 | 7 | module.exports = { 8 | renderCards: function(cardData) { 9 | let cards = []; 10 | let items = cardData; 11 | if (items) { 12 | Object.keys(items).forEach( (item, index) => { 13 | 14 | let currentCard = items[item]; 15 | cards.push( 16 | 17 | 18 | ) 19 | }); 20 | } 21 | return cards; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /views/pages/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= title%> 4 | 5 | 6 | 7 |
<%- html%>
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var Webpack = require('webpack'); 2 | var path = require('path'); 3 | var nodeModulesPath = path.resolve(__dirname, 'node_modules'); 4 | var buildPath = path.resolve(__dirname, 'public/build/'); 5 | var serverPath = path.resolve(__dirname, 'src/server', 'app.js'); 6 | var browserPath = path.resolve(__dirname, 'src/client', 'main.js'); 7 | 8 | var definePlugin = new Webpack.DefinePlugin({ 9 | __DEV__: JSON.stringify( 10 | JSON.parse(process.env.BUILD_DEV || 'true') 11 | ), 12 | __PROD__: JSON.stringify( 13 | JSON.parse(process.env.BUILD_PROD || 'false') 14 | ) 15 | }); 16 | 17 | module.exports = [ 18 | { 19 | target: 'node', 20 | devtool: 'source-map', 21 | debug: true, 22 | entry: { 23 | app: [serverPath] 24 | }, 25 | output: { 26 | path: buildPath, 27 | filename: 'server.js', 28 | libraryTarget: 'umd' 29 | }, 30 | externals: [/^[a-z\-0-9]+$/, 'react-router/lib/Location.js', 'react-dom/server'], 31 | module: { 32 | loaders: [ 33 | { 34 | test: /\.json$/, 35 | loader: 'json-loader' 36 | }, 37 | { 38 | test: /\.jsx?$/, 39 | exclude: /nodeModulesPath/, 40 | loader: 'babel' 41 | }, 42 | { 43 | test: /\.ejs$/, 44 | loader: 'ejs-loader?variable=data' 45 | } 46 | ] 47 | }, 48 | resolve: { 49 | extensions: ['', '.js', '.json', '.jsx'], 50 | alias: { 51 | '$history': 'history/lib/createLocation' 52 | } 53 | }, 54 | plugins: [definePlugin] 55 | }, 56 | { 57 | devtool: 'eval', 58 | debug: true, 59 | entry: { 60 | app:['./src/client/main.js'] 61 | }, 62 | output: { 63 | path: buildPath, 64 | publicPath: '/build/', 65 | filename: 'browser.js' 66 | }, 67 | module: { 68 | loaders: [ 69 | { 70 | test: /\.json$/, 71 | loader: 'json-loader', 72 | exclude: /nodeModulesPath/ 73 | }, 74 | { 75 | test: /\.jsx?$/, 76 | exclude: /nodeModulesPath/, 77 | loader: 'babel-loader' 78 | }, 79 | { 80 | test: /\.ejs$/, 81 | loader: 'ejs-loader?variable=data', 82 | exclude: /nodeModulesPath/ 83 | } 84 | ] 85 | }, 86 | resolve: { 87 | extensions: ['', '.js', '.json', '.jsx'], 88 | alias: { 89 | '$history':'history/lib/createBrowserHistory' 90 | } 91 | }, 92 | plugins: [new Webpack.NoErrorsPlugin(), definePlugin] 93 | } 94 | ]; 95 | --------------------------------------------------------------------------------