Skip to content

Instantly share code, notes, and snippets.

@StevenLangbroek
Created May 8, 2016 09:10
Show Gist options
  • Save StevenLangbroek/08d420f73f0c09178de3ae7b5e87d88e to your computer and use it in GitHub Desktop.
Save StevenLangbroek/08d420f73f0c09178de3ae7b5e87d88e to your computer and use it in GitHub Desktop.
SSR With React
import express from 'express';
import render from './react';
const app = express();
app.use(render);
import prepareLocals from './prepareLocals';
import matchRoutes from './matchRoutes';
import prefetch from './prefetch';
import renderComponent from './renderComponent';
import renderPage from './renderPage';
export default [
prepareLocals,
matchRoutes,
prefetch(__DISABLE_SSR__),
renderComponent(__DISABLE_SSR__),
renderPage,
];
import createHistory from 'history/lib/createMemoryHistory';
import merge from 'lodash/merge';
import createApiClient from 'helpers/createApiClient';
import createStore from 'store';
import createRoutes from 'routes';
import { setAcceptCookies } from 'reducers/ui';
const debug = require('debug')('react:prepareLocals');
/**
* Rendering with React in Node isn't terribly difficult, you just need to break
* everything up in smaller steps. This step takes care of creating our store,
* setting up React's router (using MemoryHistory, cause, you know, Node),
* giving our request access to Webpack's asset manifest etc. We store everything
* we need under res.locals, and then moving on to the next step.
* @param {Object} req Express request object
* @param {Object} res Express response object
* @param {Function} next Express middleware dispatcher
*/
export default (req, res, next) => {
debug('==========================');
debug('Preparing response locals.');
if (__DEVELOPMENT__) {
// Do not cache webpack stats: the script file would change since
// hot module replacement is enabled in the development env
webpackIsomorphicTools.refresh();
}
const client = createApiClient(req);
const location = req.originalUrl;
const history = createHistory(location);
const store = createStore(history, client);
const routes = createRoutes(store);
const assets = webpackIsomorphicTools.assets();
res.meta = (res.meta || {});
merge(res.locals, {
client,
history,
store,
routes,
location,
assets,
propertyId,
});
debug('Finished preparing response locals. ');
next();
};
import { match } from 'react-router';
const debug = require('debug')('react:matchRoutes');
/**
* We clearly only want to handle routes that React is aware of with React,
* so we take the routes we generated in the previous step, and run them through
* React Router's `match` utility.
* @param {Object} req Express request object
* @param {Object} res Express response object
* @param {Function} next Express middleware dispatcher
*/
export default (req, res, next) => {
debug(`Beginning route matching for: ${res.locals.location}`);
match({
history: res.locals.history,
routes: res.locals.routes,
location: res.locals.location,
}, (error, redirectLocation, renderProps) => {
if (redirectLocation) {
const targetUrl = redirectLocation.pathname + redirectLocation.search;
debug(`Redirecting to new location: ${targetUrl}`);
res.redirect(targetUrl);
}
if (error) {
debug('Router error');
error.status = 500;
return next(error);
}
if (!renderProps) {
debug('No matching route found');
const noRouteError = new Error('No matching route found.');
noRouteError.status = 404;
return next(noRouteError);
}
debug('Succesfully matched routes.');
res.locals.renderProps = renderProps;
return next();
});
};
import { trigger } from 'redial';
const debug = require('debug')('react:prefetch');
export default (disableSsr) => async (req, res, next) => {
debug('Beginning prefetch step.');
const hasRenderProps = !!res.locals.renderProps;
const shouldSkipPrefetch = (disableSsr || !hasRenderProps);
if (shouldSkipPrefetch) {
debug('Skipping prefetch.');
res.meta.hydrated = false;
next();
return;
}
const { renderProps, store } = res.locals;
const fetchLocals = {
path: renderProps.location.pathname,
query: renderProps.location.query,
params: renderProps.params,
// Allow lifecycle hooks to dispatch Redux actions:
dispatch: store.dispatch,
getState: store.getState,
};
debug('Starting prefetch on matched components.');
const { components } = renderProps;
// Wait for all async actions to be dispatched.
await trigger('fetch', components, fetchLocals);
debug('Done prefetching components');
res.meta.hydrated = true;
next();
};
import React from 'react';
import { Provider } from 'react-redux';
import { RouterContext } from 'react-router';
const debug = require('debug')('react:renderComponent');
export default (disableSsr) => (req, res, next) => {
debug('Rendering component.');
if (disableSsr) {
debug('Skipping component render.');
return next();
}
const {
store,
renderProps,
} = res.locals;
res.locals.component = (
<Provider store={store}>
<RouterContext {...renderProps} />
</Provider>
);
debug('Done rendering component. ');
return next();
};
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import Document from 'containers/Document';
const debug = require('debug')('react:renderPage');
const DOCTYPE = '<!doctype html>';
export default (req, res) => {
debug('Beginning renderPage');
const {
locals,
meta,
} = res;
const {
assets,
store,
component,
propertyId,
} = locals;
res.type('text/html');
return res.send(`
${DOCTYPE}
${renderToStaticMarkup(
<Document
assets={assets}
meta={meta}
store={store}
component={component}
/>
)}
`);
};
/* eslint-disable max-len */
import React, { PropTypes } from 'react';
import { renderToString } from 'react-dom/server';
import Helmet from 'react-helmet';
const storeShape = PropTypes.shape({
getState: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
});
/**
* Wrapping component for the entire document. Interacts with
* Webpack Isomorphic Tools and React Helmet to build metadata
* and include assets.
* @name Document
* @param {Object} props Props:
* @param {Object} props.assets Webpack asset manifest converted to an Object
* @param {Object} props.component React component to render into outlet ('#content')
* @param {Object} props.store Redux store instance
* @return {ReactElement} HTML document minus doctype declaration
*/
const Document = ({
assets,
component,
store,
meta,
}) => {
const content = component ? renderToString(component) : '';
const head = Helmet.rewind();
return (
<html lang="en-us">
<head>
{head.base.toComponent()}
{head.title.toComponent()}
{head.meta.toComponent()}
{head.link.toComponent()}
{head.script.toComponent()}
<link rel="shortcut icon" href="/static/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1" />
{/* styles (will be present only in production with webpack extract text plugin) */}
{Object.keys(assets.styles).map((style, key) =>
<link href={assets.styles[style]} key={key} media="screen, projection" rel="stylesheet" type="text/css" charSet="UTF-8" />
)}
</head>
<body>
<div id="content" dangerouslySetInnerHTML={{ __html: content }} />
<script dangerouslySetInnerHTML={{ __html: `window.__INITIAL_STATE__ = ${JSON.stringify(store.getState().toJS())}` }} />
<script dangerouslySetInnerHTML={{ __html: `window.__META__ = ${JSON.stringify(meta)}` }} />
<script src={assets.javascript.main} charSet="UTF-8" />
</body>
</html>
);
};
Document.propTypes = {
assets: PropTypes.object.isRequired,
component: PropTypes.node,
store: storeShape,
meta: PropTypes.object
};
export default Document;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment