├── .gitignore ├── .babelrc ├── src ├── reducers │ ├── index.js │ ├── Router.js │ └── info.js ├── api │ ├── routes │ │ ├── index.js │ │ └── loadInfo.js │ └── api.js ├── actions │ ├── actionTypes.js │ └── infoActions.js ├── components │ ├── index.js │ ├── MainMenu.js │ ├── App.js │ ├── InfoBar.js │ └── AppTreeFactory.js ├── views │ ├── index.js │ ├── Home.js │ ├── Error404NotFound.js │ ├── Another.js │ └── DefaultHeader.js ├── redux │ ├── thunkMiddleware.js │ ├── create.js │ └── clientMiddleware.js ├── config.js ├── client.js ├── router.js ├── ApiClient.js └── server.js ├── favicon.ico ├── babel.server.js ├── loaders.js ├── webpack.client.js ├── webpack.client-watch.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | static 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxPragma": "element" 3 | } 4 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | export info from './info'; 2 | -------------------------------------------------------------------------------- /src/api/routes/index.js: -------------------------------------------------------------------------------- 1 | export loadInfo from './loadInfo'; 2 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilsivanson/deku-redux-universal-hot-example/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const INFO_LOAD = 'INFO_LOAD'; 2 | export const INFO_LOAD_SUCCESS = 'INFO_LOAD_SUCCESS'; 3 | export const INFO_LOAD_FAIL = 'INFO_LOAD_FAIL'; -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export App from './App'; 2 | export AppTreeFactory from './AppTreeFactory'; 3 | export InfoBar from './InfoBar'; 4 | export MainMenu from './MainMenu'; 5 | -------------------------------------------------------------------------------- /src/views/index.js: -------------------------------------------------------------------------------- 1 | export DefaultHeader from './DefaultHeader'; 2 | export Home from './Home'; 3 | export Another from './Another'; 4 | export Error404NotFound from './Error404NotFound'; 5 | -------------------------------------------------------------------------------- /src/reducers/Router.js: -------------------------------------------------------------------------------- 1 | const initialState = {}; 2 | 3 | export default function Router(state = initialState, action = {}) { 4 | switch (action.type) { 5 | 6 | } 7 | return state; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/routes/loadInfo.js: -------------------------------------------------------------------------------- 1 | export default function (req) { 2 | return new Promise(function (resolve) { 3 | resolve({ 4 | message: 'Hello from the API server', 5 | time: Date.now() 6 | }); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/redux/thunkMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function thunkMiddleware({ dispatch, getState }) { 2 | return (next) => (action) => 3 | typeof action === 'function' ? 4 | action(dispatch, getState) : 5 | next(action); 6 | } 7 | -------------------------------------------------------------------------------- /src/views/Home.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | import {InfoBar} from '../components/index'; 3 | 4 | function render({props}) { 5 | return ( 6 |
7 |

Welcome

8 | 9 |
10 | ); 11 | } 12 | 13 | export default {render}; 14 | -------------------------------------------------------------------------------- /src/views/Error404NotFound.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | 3 | function render() { 4 | return ( 5 |
6 |
7 | Error 404 8 |
9 |
10 | ); 11 | } 12 | 13 | export default {render: render}; 14 | -------------------------------------------------------------------------------- /babel.server.js: -------------------------------------------------------------------------------- 1 | require('babel/register')({ 2 | stage: 0, 3 | plugins: ['typecheck'] 4 | }); 5 | 6 | /** 7 | * Define isomorphic constants. 8 | */ 9 | global.__CLIENT__ = false; 10 | global.__SERVER__ = true; 11 | 12 | if (process.env.NODE_ENV !== 'production') { 13 | if (!require('piping')({hook: true})) { 14 | return; 15 | } 16 | } 17 | 18 | require('./src/server'); 19 | -------------------------------------------------------------------------------- /src/views/Another.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | import {InfoBar} from '../components/index'; 3 | 4 | function render({props}) { 5 | return ( 6 |
7 |

Another page

8 |

Note that the info bar state persists. It is stored in redux and whenever it changes it is pushed to currentReduxState. See client.js for more information.

9 | 10 |
11 | ); 12 | } 13 | 14 | export default {render}; 15 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | isProduction: false, 4 | port: 3000, 5 | apiPort: 3030, 6 | static: 'http://localhost:3031', 7 | app: { 8 | name: 'Deku Redux Example Development' 9 | } 10 | }, 11 | production: { 12 | isProduction: true, 13 | port: process.env.PORT, 14 | apiPort: 3030, 15 | static: '', 16 | app: { 17 | name: 'Deku Redux Example Production' 18 | } 19 | } 20 | }[process.env.NODE_ENV || 'development']; 21 | -------------------------------------------------------------------------------- /src/redux/create.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 2 | import clientMiddleware from './clientMiddleware'; 3 | import thunkMiddleware from './thunkMiddleware'; 4 | import * as reducers from '../reducers/index'; 5 | 6 | export default function (client, data) { 7 | const reducer = combineReducers(reducers); 8 | const finalCreateStore = applyMiddleware(thunkMiddleware, clientMiddleware(client))(createStore); 9 | return finalCreateStore(reducer, data); 10 | } 11 | -------------------------------------------------------------------------------- /src/actions/infoActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | INFO_LOAD, 3 | INFO_LOAD_SUCCESS, 4 | INFO_LOAD_FAIL 5 | } from './actionTypes'; 6 | 7 | export function load() { 8 | return { 9 | types: [INFO_LOAD, INFO_LOAD_SUCCESS, INFO_LOAD_FAIL], 10 | promise: (client) => loadFromServer(client) 11 | }; 12 | } 13 | 14 | function loadFromServer(client) { 15 | return client.get('/loadInfo') 16 | .then((result) => { 17 | return result; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/MainMenu.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | 3 | export const propTypes = { 4 | router: {type: 'object', source: 'router'} 5 | }; 6 | 7 | export function render({props}) { 8 | let {router} = props; 9 | return ( 10 | 15 | ) 16 | } 17 | 18 | export default {propTypes, render}; 19 | -------------------------------------------------------------------------------- /src/redux/clientMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function clientMiddleware(client) { 2 | return ({/* dispatch, getState */}) => { 3 | return (next) => (action) => { 4 | const {promise, types, ...rest} = action; 5 | if (!promise) { 6 | return next(action); 7 | } 8 | 9 | const [REQUEST, SUCCESS, FAILURE] = types; 10 | next({...rest, type: REQUEST}); 11 | return promise(client).then( 12 | (result) => next({...rest, result, type: SUCCESS}), 13 | (error) => next({...rest, error, type: FAILURE}) 14 | ); 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | import {MainMenu} from './index'; 3 | 4 | export const propTypes = { 5 | header: {type: 'object', source: 'currentHeader'}, 6 | page: {type: 'object', source: 'currentPage'} 7 | }; 8 | 9 | export function render({props}) { 10 | let {page, header} = props; 11 | 12 | return ( 13 |
14 |
15 | {header} 16 | 17 |
18 |
19 |
{page}
20 |
21 |
22 | ) 23 | } 24 | 25 | export default {propTypes, render}; 26 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import {element, render} from 'deku'; 2 | import {AppTreeFactory} from './components/index'; 3 | 4 | // build the app tree 5 | const appTree = AppTreeFactory(window.__state); 6 | delete window.__state; 7 | 8 | // start rendering the app 9 | const {remove, inspect} = render(appTree, document.getElementById('content')); 10 | 11 | if (module.hot) { 12 | module.hot.accept(); 13 | module.hot.dispose(function() { 14 | // get current state and store as if sending from server 15 | window.__state = appTree.sources.store.getState(); 16 | 17 | // unsubscribe from current store 18 | appTree.sources.storeUnsubscribe(); 19 | 20 | // teardown last app before rendering new one 21 | remove(); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /loaders.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | {include: /\.json$/, loaders: ["json-loader"]}, 3 | {include: /\.png$/, loader: 'url', query: {mimetype: 'image/png', limit: 10240}}, 4 | {include: /\.jpg$/, loader: 'url', query: {mimetype: 'image/jpg', limit: 10240}}, 5 | {include: /\.gif$/, loader: 'url', query: {mimetype: 'image/gif', limit: 10240}}, 6 | {include: /\.md$/, loader: "html!markdown"}, 7 | {include: /\.svg$/, loader: 'url', query: {mimetype: 'image/svg+xml', limit: 10240}}, 8 | { 9 | include: /\.woff(\?v=[0-9]\.[0-9]\.[0-9])?$/, 10 | loader: 'url', 11 | query: {mimetype: 'application/font-woff', limit: 10240} 12 | }, 13 | { 14 | include: /\.woff2(\?v=[0-9]\.[0-9]\.[0-9])?$/, 15 | loader: 'url', 16 | query: {mimetype: 'application/font-woff', limit: 10240} 17 | }, 18 | {include: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader"} 19 | ]; 20 | module.exports.css = 'css?importLoaders=1!autoprefixer!sass'; 21 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import config from '../config'; 3 | import * as actions from './routes/index'; 4 | 5 | const app = express(); 6 | 7 | module.exports = function () { 8 | return new Promise((resolve, reject) => { 9 | app.use(function (req, res) { 10 | var matcher = /\/([^?]+)/.exec(req.url), 11 | action = matcher && actions[matcher[1]]; 12 | if (action) { 13 | action(req) 14 | .then(function (result) { 15 | res.json(result); 16 | }, function (reason) { 17 | if (reason && reason.redirect) { 18 | res.redirect(reason.redirect); 19 | } else { 20 | console.error('API ERROR:', reason); 21 | res.status(reason.status || 500).json(reason); 22 | } 23 | }); 24 | } else { 25 | res.status(404).end('NOT FOUND'); 26 | } 27 | }); 28 | app.listen(config.apiPort); 29 | resolve(); 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/reducers/info.js: -------------------------------------------------------------------------------- 1 | import { 2 | INFO_LOAD, 3 | INFO_LOAD_SUCCESS, 4 | INFO_LOAD_FAIL 5 | } from '../actions/actionTypes'; 6 | 7 | const initialState = { 8 | loaded: false 9 | }; 10 | 11 | export default function info(state = initialState, action = {}) { 12 | switch (action.type) { 13 | case INFO_LOAD: 14 | return { 15 | ...state, 16 | loading: true 17 | }; 18 | case INFO_LOAD_SUCCESS: 19 | return { 20 | ...state, 21 | loading: false, 22 | loaded: true, 23 | data: action.result, 24 | error: null 25 | }; 26 | case INFO_LOAD_FAIL: 27 | return { 28 | ...state, 29 | loading: false, 30 | loaded: false, 31 | data: null, 32 | error: action.error 33 | }; 34 | } 35 | return state; 36 | }; 37 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | import {DefaultHeader, Home, Another, Error404NotFound} from './views/index'; 3 | 4 | export function AppRouter(router) { 5 | return function (app) { 6 | // ignore state on router 7 | router.routed.add(() => { 8 | router.resetState(); 9 | }); 10 | 11 | // set the router on the app so that we can access it from components 12 | app.set('router', router); 13 | 14 | // set a default header for the app 15 | app.set('currentHeader', ); 16 | 17 | router.addRoute('/', () => { 18 | app.set('currentPage', ); 19 | }); 20 | router.addRoute('/another', () => { 21 | app.set('currentPage', ); 22 | }); 23 | router.addRoute('/404', () => { 24 | app.set('currentPage', ); 25 | }); 26 | 27 | // no route found on parse 28 | router.bypassed.add(function (request) { 29 | console.log('No route matches, so parsing /404', request); 30 | router.parse('/404'); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/views/DefaultHeader.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | 3 | function render({props}) { 4 | return ( 5 | 26 | ); 27 | } 28 | 29 | export default {render}; 30 | -------------------------------------------------------------------------------- /webpack.client.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var loaders = require('./loaders'); 4 | 5 | module.exports = { 6 | name: 'browser', 7 | target: 'web', 8 | cache: false, 9 | context: __dirname, 10 | devtool: false, 11 | entry: ['./src/client'], 12 | output: { 13 | path: path.join(__dirname, 'static/dist'), 14 | filename: 'client.js', 15 | chunkFilename: 'client.[name].js', 16 | publicPath: 'dist/' 17 | }, 18 | module: { 19 | loaders: loaders.concat([ 20 | { 21 | include: /\.js$/, 22 | loaders: ['babel-loader?stage=0&optional=runtime&plugins=typecheck'], 23 | exclude: /node_modules/ 24 | }]) 25 | }, 26 | plugins: [ 27 | new webpack.DefinePlugin({__CLIENT__: true, __SERVER__: false}), 28 | new webpack.DefinePlugin({'process.env': {NODE_ENV: '"production"'}}), 29 | new webpack.optimize.DedupePlugin(), 30 | new webpack.optimize.OccurenceOrderPlugin(), 31 | new webpack.optimize.UglifyJsPlugin() 32 | ], 33 | resolve: { 34 | modulesDirectories: [ 35 | 'src', 36 | 'node_modules' 37 | ], 38 | extensions: ['', '.json', '.js'] 39 | }, 40 | node: { 41 | __dirname: true, 42 | fs: 'empty' 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /webpack.client-watch.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var config = require('./webpack.client.js'); 5 | var loaders = require('./loaders'); 6 | 7 | config.cache = true; 8 | config.debug = true; 9 | config.devtool = 'eval'; 10 | 11 | config.entry.unshift( 12 | 'webpack-dev-server/client?http://localhost:8080', 13 | 'webpack/hot/only-dev-server' 14 | ); 15 | 16 | config.output.publicPath = 'http://localhost:8080/dist/'; 17 | config.output.hotUpdateMainFilename = 'update/[hash]/update.json'; 18 | config.output.hotUpdateChunkFilename = 'update/[hash]/[id].update.js'; 19 | 20 | config.plugins = [ 21 | new webpack.DefinePlugin({__CLIENT__: true, __SERVER__: false}), 22 | new webpack.HotModuleReplacementPlugin(), 23 | new webpack.NoErrorsPlugin() 24 | ]; 25 | 26 | config.module = { 27 | loaders: loaders.concat([ 28 | { 29 | include: /\.js$/, 30 | loaders: ['babel-loader?stage=0&optional=runtime&plugins=typecheck'], 31 | exclude: /node_modules/ 32 | }]) 33 | }; 34 | 35 | config.devServer = { 36 | publicPath: 'http://localhost:8080/dist/', 37 | contentBase: './static', 38 | hot: true, 39 | inline: true, 40 | lazy: false, 41 | quiet: true, 42 | noInfo: false, 43 | headers: {'Access-Control-Allow-Origin': '*'}, 44 | stats: {colors: true}, 45 | host: '0.0.0.0' 46 | }; 47 | 48 | module.exports = config; 49 | -------------------------------------------------------------------------------- /src/components/InfoBar.js: -------------------------------------------------------------------------------- 1 | import {element} from 'deku'; 2 | import {bindActionCreators} from 'redux'; 3 | import * as infoActions from '../actions/infoActions'; 4 | 5 | const propTypesInfoBar = { 6 | load: {type: 'function', source: 'load'}, 7 | info: {type: 'object', source: 'info'} 8 | }; 9 | 10 | function renderInfoBar({props}) { 11 | let {load, info} = props; 12 | if (!info) { 13 | info = {}; 14 | } 15 | return ( 16 |
17 |
18 | Remote call with redux 19 |
Loading: {info.loading ? 'TRUTHY' : 'FALSEY'} 20 |
Loaded: {info.loaded ? 'TRUTHY' : 'FALSEY'} 21 |
Data: {info.data ? info.data.message + ' ' + new Date(info.data.time).toString() : 'NO DATA'} 22 |
Errors: {info.error ? 'YEAH' : 'NOPE'} 23 |

24 | Load 25 |
26 |
27 | ); 28 | } 29 | 30 | const InfoBar = {propTypes: propTypesInfoBar, render: renderInfoBar}; 31 | 32 | const propTypes = { 33 | store: {type: 'object', source: 'store'}, 34 | info: {type: 'object', source: 'info'} 35 | }; 36 | 37 | function render({props}) { 38 | const {store, info} = props; 39 | return ( 40 | 41 | ); 42 | } 43 | 44 | export default {propTypes, render}; 45 | -------------------------------------------------------------------------------- /src/ApiClient.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | import config from 'config'; 3 | 4 | function formatUrl(path) { 5 | var url; 6 | if (path[0] !== '/') { 7 | path = '/' + path; 8 | } 9 | if (__SERVER__) { 10 | // Prepend host and port of the API server to the path. 11 | url = 'http://localhost:' + config.apiPort + path; 12 | } else { 13 | // Prepend `/api` to relative URL, to proxy to API server. 14 | url = '/api' + path; 15 | } 16 | return url; 17 | } 18 | 19 | export default class ApiClient { 20 | constructor(req) { 21 | ['get', 'post', 'put', 'patch', 'del']. 22 | forEach((method) => { 23 | this[method] = (path, options) => { 24 | return new Promise((resolve, reject) => { 25 | var request = superagent[method](formatUrl(path)); 26 | if (options && options.params) { 27 | request.query(options.params); 28 | } 29 | if (__SERVER__) { 30 | request.set('cookie', req.get('cookie')); 31 | } 32 | if (options && options.data) { 33 | request.send(options.data); 34 | } 35 | request.end((err, res) => { 36 | if (err) { 37 | reject(err); 38 | } else { 39 | resolve(res.body); 40 | } 41 | }); 42 | }); 43 | }; 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deku Redux Universal Hot Example 2 | ================================= 3 | 4 | ### Description 5 | 6 | NOTE: This is a fork of [erikras/react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) modified for Deku 7 | 8 | Beware of dirty hacks. 9 | 10 | * Isomorphic/Universal rendering 11 | * Both client and server make calls to load data from separate API server 12 | * [Deku](https://github.com/dekujs/deku) 13 | * [Crossroads Router](https://github.com/millermedeiros/crossroads.js) 14 | * [Materialize CSS](http://materializecss.com/) due to lack of material-ui components 15 | * [Express](http://expressjs.com) 16 | * [Babel](http://babeljs.io) for ES6 and ES7 magic 17 | * [Webpack](http://webpack.github.io) for bundling 18 | * [Webpack Dev Server](http://webpack.github.io/docs/webpack-dev-server.html) 19 | * [Redux](https://github.com/gaearon/redux)'s futuristic [Flux](https://facebook.github.io/react/blog/2014/05/06/flux 20 | .html) implementation 21 | 22 | ### Running Web Server 23 | 24 | ``` 25 | npm install 26 | npm run start 27 | ``` 28 | 29 | ### Running Webpack Dev Server 30 | 31 | ``` 32 | npm run watch-client 33 | ``` 34 | 35 | Both `npm run start` and `npm run watch-client` must be running at the same 36 | time for the webapp to work with hot reloading. 37 | 38 | Then try editing src/components/App.js or any other template or store 39 | 40 | ### Maybe TODO 41 | 42 | * Move routing solution into redux to get rid of hacky solution 43 | * Make sure all promises if any are resolved before rendering on server 44 | * Get rid of AppClient is not defined error (that was also present in original repo) 45 | 46 | ----- 47 | 48 | Original Author: Erik Rasmussen [@erikras](https://twitter.com/erikras) 49 | 50 | Modified by: Nils Ivanson [@nivanson](https://github.com/nivanson) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deku-redux-universal-hot-example", 3 | "description": "Example of an isomorphic (universal) webapp using deku redux and hot reloading based on @erikras excellent https://github.com/erikras/react-redux-universal-hot-example/", 4 | "author": "Nils Ivanson (http://github.com/nivanson)", 5 | "version": "0.0.1", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/nivanson/deku-redux-universal-hot-example.git" 9 | }, 10 | "homepage": "https://github.com/nivanson/deku-redux-universal-hot-example", 11 | "main": "babel.server.js", 12 | "scripts": { 13 | "start": "NODE_PATH=\"./src\" node ./babel.server", 14 | "start-production": "NODE_PATH=\"./src\" NODE_ENV=\"production\" PORT=\"8080\" node ./babel.server", 15 | "build": "node ./node_modules/webpack/bin/webpack.js --verbose --colors --display-error-details --config webpack.client.js", 16 | "watch-client": "node ./node_modules/webpack/bin/webpack.js --verbose --colors --display-error-details --config webpack.client-watch.js && node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config webpack.client-watch.js", 17 | "watch": "node ./node_modules/concurrently/src/main.js --kill-others \"npm run watch-client\" \"npm run start\"" 18 | }, 19 | "dependencies": { 20 | "babel": "5.4.7", 21 | "babel-plugin-typecheck": "0.0.3", 22 | "compression": "^1.5.0", 23 | "crossroads": "^0.12.0", 24 | "deku": "^0.4.5", 25 | "express": "^4.13.0", 26 | "express-session": "^1.11.3", 27 | "file-loader": "^0.8.4", 28 | "http-proxy": "^1.11.1", 29 | "piping": "0.1.8", 30 | "redux": "1.0.0-rc", 31 | "serve-favicon": "^2.3.0", 32 | "serve-static": "^1.10.0", 33 | "superagent": "^1.2.0", 34 | "url-loader": "^0.5.6" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "5.4.7", 38 | "babel-loader": "5.1.3", 39 | "babel-runtime": "5.4.7", 40 | "concurrently": "0.1.1", 41 | "json-loader": "0.5.2", 42 | "webpack": "^1.9.11", 43 | "webpack-dev-server": "1.9.0" 44 | }, 45 | "engines": { 46 | "node": ">=0.10.32" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/AppTreeFactory.js: -------------------------------------------------------------------------------- 1 | import {App} from './index'; 2 | import {element, deku} from 'deku'; 3 | import crossroads from 'crossroads'; 4 | import {AppRouter} from '../router'; 5 | import createRedux from '../redux/create'; 6 | import ApiClient from '../ApiClient'; 7 | 8 | // * state is the state of the application 9 | // to start a new application with an old state just store.getState() and inject 10 | // * request is the express request that is given on server side rendering 11 | // since this is undefined on browser side we also be know that code is 12 | // running in the browser when it is undefined 13 | export default function AppTreeFactory(state, request) { 14 | const isServerSide = !!request; 15 | const client = new ApiClient(request); 16 | const store = createRedux(client, state); 17 | 18 | const router = crossroads.create(); 19 | router.go = (path) => { 20 | if (!isServerSide) history.pushState(store.getState(), path, path); 21 | router.parse(path); 22 | }; 23 | // generateGo is used for click handlers in components and views 24 | router.generateGo = (path) => { 25 | return (event) => { 26 | const preventDefault = event && !(event.shiftKey || event.ctrlKey || event.metaKey); 27 | if (preventDefault) { 28 | event.preventDefault(); 29 | } 30 | router.go(path); 31 | } 32 | }; 33 | // history handler for client 34 | if (!isServerSide) { 35 | window.onpopstate = function (event) { 36 | router.parse(location.pathname); 37 | }; 38 | } 39 | 40 | const appTree = deku().use(AppRouter(router)); 41 | if (isServerSide) appTree.option('validateProps', true); 42 | appTree.set('store', store); 43 | updateState(appTree); 44 | 45 | // subscribe app tree to store changes in state and store a handler for teardown 46 | const unsubscribeStore = store.subscribe(() => updateState(appTree)); 47 | appTree.set('storeUnsubscribe', unsubscribeStore); 48 | 49 | // mount the application container on the application tree 50 | appTree.mount(); 51 | 52 | // parse current path 53 | if (isServerSide) { 54 | router.parse(request.path); 55 | } else { 56 | router.parse(location.pathname); 57 | } 58 | 59 | return appTree; 60 | } 61 | 62 | function updateState(appTree) { 63 | const store = appTree.sources.store; 64 | const state = store.getState(); 65 | 66 | // set minimal values on the app tree for each store so that we can use them 67 | // directly on props 68 | const keys = Object.keys(state); 69 | keys.forEach((key) => { 70 | appTree.set(key, state[key]); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import Express from 'express'; 2 | import {element, tree, renderString} from 'deku'; 3 | import {AppTreeFactory} from './components'; 4 | import config from './config'; 5 | import favicon from 'serve-favicon'; 6 | import compression from 'compression'; 7 | import httpProxy from 'http-proxy'; 8 | import url from 'url'; 9 | import path from 'path'; 10 | import api from './api/api'; 11 | 12 | const server = new Express(); 13 | const webserver = process.env.NODE_ENV === 'production' ? '' : '//localhost:8080'; 14 | const proxy = httpProxy.createProxyServer({ 15 | target: 'http://localhost:' + config.apiPort 16 | }); 17 | 18 | server.use(compression()); 19 | server.use(require('serve-static')(path.join(__dirname, '..', 'static'))); 20 | server.use(favicon(path.join(__dirname, '..', 'favicon.ico'))); 21 | 22 | // Proxy to API server 23 | server.use('/api', (request, response) => { 24 | proxy.web(request, response); 25 | }); 26 | 27 | server.use((request, response, next) => { 28 | let appTree = AppTreeFactory(undefined, request); 29 | 30 | // inject some materialize css constructs 31 | let stylesheets = ` 32 | #slide-out a { 33 | height: 40px; 34 | line-height: 40px; 35 | } 36 | 37 | header, main, footer { 38 | padding-left: 240px; 39 | } 40 | 41 | @media only screen and (max-width : 992px) { 42 | header, main, footer { 43 | padding-left: 0; 44 | } 45 | } 46 | `; 47 | 48 | // and some materialize css js too 49 | let javascripts = ` 50 | // Initialize collapse button 51 | $(function() { 52 | $(".button-collapse").sideNav(); 53 | // Initialize collapsible (uncomment the line below if you use the dropdown variation) 54 | //$('.collapsible').collapsible(); 55 | }); 56 | `; 57 | 58 | // TODO: somehow make sure all components have their data before proceeding 59 | 60 | let state = appTree.sources.currentReduxState; 61 | 62 | // TODO: implement handler for 404 with status code 63 | // TODO: implement handler for 500 with status code 64 | 65 | let htmlTree = tree( 66 | 67 | 68 | Deku Redux Universal Hot Example 69 | 71 | 74 | 75 | 77 | 78 | 79 |