Skip to content

Instantly share code, notes, and snippets.

@smontlouis
Last active August 29, 2015 14:25
Show Gist options
  • Save smontlouis/fbcd61fe004771f3ca63 to your computer and use it in GitHub Desktop.
Save smontlouis/fbcd61fe004771f3ca63 to your computer and use it in GitHub Desktop.
// import favicon from 'serve-favicon';
import compression from 'compression';
import config from '../config';
import esteHeaders from '../lib/estemiddleware';
import express from 'express';
import intlMiddleware from '../lib/intlmiddleware';
import render from './render';
import userState from './userstate';
import fetchData from './fetchData';
const app = express();
// Add Este.js headers for React related routes only.
if (!config.isProduction)
app.use(esteHeaders());
app.use(compression());
// TODO: Add favicon.
// app.use(favicon('assets/img/favicon.ico'))
// TODO: Move to CDN.
app.use('/build', express.static('build'));
app.use('/assets', express.static('assets'));
// Load translations, fallback to defaultLocale if no translation is available.
app.use(intlMiddleware({
defaultLocale: config.defaultLocale
}));
// Load state extras for current user.
//app.use(userState());
/**
* 1. All the magic will be here
*/
app.use(fetchData());
app.get('*', (req, res, next) => {
render(req, res).catch(next);
});
app.on('mount', () => {
console.log('App is available at %s', app.mountpath);
});
export default app;
import Promise from 'bluebird';
import Immutable from 'immutable';
import Router from 'react-router';
import Flux from '../../client/lib/flux/flux';
import store from '../../client/app/store';
import initialState from '../initialstate';
import stateMerger from '../lib/merger';
import routes from '../../client/routes';
import allActions from '../../client/app/allActions';
/**
* 2. I'm doing shit like loading all actions, init flux, and init router. Maybe it's dirty, dunno.
*/
export default function fetchData() {
return (req, res, next) => {
let appState = Immutable.fromJS(initialState).mergeWith(stateMerger, req.userState, {intl: req.intl}).toJS();
const flux = new Flux(store, appState);
const actions = allActions.reduce((actions, {feature, create}) => {
const dispatch = (action, payload) => flux.dispatch(action, payload, {feature});
const featureActions = create(dispatch);
return {...actions, [feature]: featureActions};
}, {});
Router.run(routes, req.originalUrl, (Root, routerState) => {
Promise.all(routerState.routes
.filter(route => route.handler.fetchData)
.map(route => {
return route.handler.fetchData(actions); //Pass actions here - Maybe there is a better solution. Honestly I've no idea what I'm doing :)
})
).then(() => {
req.appState = flux.state.toJS(); //Get updated state
next();
});
});
};
}
import DocumentTitle from 'react-document-title';
import Html from './html.react';
import Promise from 'bluebird';
import React from 'react';
import Router from 'react-router';
import config from '../config';
import routes from '../../client/routes';
export default function render(req, res) {
return renderPage(req, res);
}
function renderPage(req, res) {
return new Promise((resolve, reject) => {
const router = Router.create({
routes,
location: req.originalUrl,
onError: reject,
onAbort: (abortReason) => {
// Some requireAuth higher order component requested redirect.
if (abortReason.constructor.name === 'Redirect') {
const {to, params, query} = abortReason;
const path = router.makePath(to, params, query);
res.redirect(path);
resolve();
return;
}
reject(abortReason);
}
});
router.run((Handler, routerState) => {
/**
* 3. We don't need appState anymore, so just pass req.appState here
*/
const html = getPageHtml(Handler, req.appState);
const notFound = routerState.routes.some(route => route.name === 'not-found');
const status = notFound ? 404 : 200;
res.status(status).send(html);
resolve();
});
});
}
function getPageHtml(Handler, appState) {
const appHtml = `<div id="app">${
React.renderToString(<Handler initialState={appState} />)
}</div>`;
const appScriptSrc = config.isProduction
? '/build/app.js?v=' + config.version
: '//localhost:8888/build/app.js';
let scriptHtml = `
<script>
(function() {
window._appState = ${JSON.stringify(appState)};
var app = document.createElement('script'); app.type = 'text/javascript'; app.async = true;
var src = '${appScriptSrc}';
// IE<11 and Safari need Intl polyfill.
if (!window.Intl) src = src.replace('.js', 'intl.js');
app.src = src;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(app, s);
})();
</script>`;
if (config.isProduction && config.googleAnalyticsId !== 'UA-XXXXXXX-X')
scriptHtml += `
<script>
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
e=o.createElement(i);r=o.getElementsByTagName(i)[0];
e.src='//www.google-analytics.com/analytics.js';
r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
ga('create','${config.googleAnalyticsId}');ga('send','pageview');
</script>`;
const title = DocumentTitle.rewind();
return '<!DOCTYPE html>' + React.renderToStaticMarkup(
<Html
bodyHtml={appHtml + scriptHtml}
isProduction={config.isProduction}
title={title}
version={config.version}
/>
);
}
import * as authActions from '../auth/actions';
import * as todosActions from '../todos/actions';
/**
* 4. Meh.
*/
export default [authActions, todosActions];
import Buttons from './buttons.react';
import Component from '../components/component.react';
import DocumentTitle from 'react-document-title';
import NewTodo from './newtodo.react';
import React from 'react';
import ToCheck from './tocheck.react';
import Todos from './todos.react';
export default class Page extends Component {
static propTypes = {
actions: React.PropTypes.object.isRequired,
msg: React.PropTypes.object.isRequired,
todos: React.PropTypes.object.isRequired
};
/**
* 5. I define a static fetchData method, taking actions as parameters, so I can use it server side too.
*/
static fetchData(actions) {
return actions.todos.loadAllTodos();
}
componentDidMount() {
const {todos, actions} = this.props;
if (!todos.list.size > 0)
Page.fetchData(actions);
}
render() {
const {
todos: {newTodo, list},
actions: {todos: actions},
msg: {todos: msg}
} = this.props;
return (
<DocumentTitle title={msg.title}>
<div className="todos-page">
<NewTodo {...{newTodo, actions, msg}} />
{/* It's just shorter syntax for:
<NewTodo actions={actions} msg={msg} newTodo={newTodo} />
*/}
<Todos {...{list, actions, msg}} />
<Buttons clearAllEnabled={list.size > 0} {...{actions, msg}} />
<ToCheck msg={msg.toCheck} />
</div>
</DocumentTitle>
);
}
}
import './app.styl';
import Component from '../components/component.react';
import Footer from './footer.react';
import Header from './header.react';
import React from 'react';
import flux from '../lib/flux';
import store from './store';
import {RouteHandler} from 'react-router';
import {createValidate} from '../validate';
/**
* 6. Meh.
*/
import actions from './allActions';
@flux(store)
export default class App extends Component {
static propTypes = {
flux: React.PropTypes.object.isRequired,
msg: React.PropTypes.object.isRequired,
users: React.PropTypes.object.isRequired
};
componentWillMount() {
this.createActions();
}
createActions() {
const {flux, msg} = this.props;
const validate = createValidate(msg);
this.actions = actions.reduce((actions, {feature, create}) => {
const dispatch = (action, payload) => flux.dispatch(action, payload, {feature});
const featureActions = create(dispatch, validate, msg[feature]);
return {...actions, [feature]: featureActions};
}, {});
}
render() {
const props = {...this.props, actions: this.actions};
const {users: {viewer}, msg} = props;
return (
<div className="page">
{/* Pass only what's needed. Law of Demeter ftw. */}
<Header msg={msg} viewer={viewer} />
<RouteHandler {...props} />
<Footer msg={msg} />
</div>
);
}
}
import Promise from 'bluebird';
export const actions = create();
export const feature = 'todos';
// When everything is constant, who needs to SCREAM_CONSTANTS?
const maxTitleLength = 42;
//TESTING
var Api = {
get: function() {
return new Promise(function(resolve, reject) {
setTimeout(() => {
const response = [{id: 'sh5s3g5baqle', title: 'meow meow meow'}, {id:'fde4fyf7p87d', title: 'make make make'}];
resolve(response);
}, 100);
});
}
};
export function create(dispatch, validate) {
return {
/**
* 7. Some fake loadTodos to test
*/
loadAllTodos() {
return new Promise((resolve, reject) => {
Api.get()
.then(res => {
dispatch(actions.loadAllTodos, res);
resolve(res);
})
.catch(err => {
reject(err);
});
});
},
addHundredTodos() {
dispatch(actions.addHundredTodos);
},
addTodo(todo) {
const title = todo.title.trim();
if (!title) return;
dispatch(actions.addTodo, todo);
},
clearAll() {
dispatch(actions.clearAll);
},
deleteTodo(todo) {
dispatch(actions.deleteTodo, todo);
},
setNewTodoField({target: {name, value}}) {
switch (name) {
case 'title':
value = value.slice(0, maxTitleLength);
break;
}
dispatch(actions.setNewTodoField, {name, value});
}
};
}
import Todo from './todo';
import getRandomString from '../lib/getrandomstring';
import {Range, Record} from 'immutable';
import {actions} from './actions';
// Records are good. https://facebook.github.io/immutable-js/docs/#/Record
const initialState = new (Record({
list: [],
newTodo: null
}));
const revive = state => initialState.merge({
list: state.get('list').map(todo => new Todo(todo)),
newTodo: new Todo(state.get('newTodo'))
});
function getRandomTodos(howMuch) {
return Range(0, howMuch).map(() => {
const id = getRandomString();
return new Todo({id, title: `Item #${id}`});
}).toArray();
}
export default function(state = initialState, action, payload) {
if (!action) state = revive(state);
switch (action) {
/**
* 8. Update state
*/
case actions.loadAllTodos:
const todos = payload.map((item) => new Todo(item));
return state.update('list', list => list.push(...todos));
case actions.addHundredTodos:
return state.update('list', list => list.push(...getRandomTodos(10)));
case actions.addTodo:
return state
.update('list', list => {
const newTodo = payload.merge({id: getRandomString()});
return list.push(newTodo);
})
.set('newTodo', new Todo);
case actions.clearAll:
return state
.update('list', list => list.clear())
.set('newTodo', new Todo);
case actions.deleteTodo:
return state.update('list', list => list.delete(list.indexOf(payload)));
case actions.setNewTodoField:
return state.setIn(['newTodo', payload.name], payload.value);
}
return state;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment