Created
May 8, 2016 09:10
-
-
Save StevenLangbroek/08d420f73f0c09178de3ae7b5e87d88e to your computer and use it in GitHub Desktop.
SSR With React
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import express from 'express'; | |
import render from './react'; | |
const app = express(); | |
app.use(render); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | |
/> | |
)} | |
`); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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