Skip to content

Instantly share code, notes, and snippets.

@ithinkdancan
Created March 21, 2019 14:20
Show Gist options
  • Save ithinkdancan/ac70e03071623260279110e6cdb85c05 to your computer and use it in GitHub Desktop.
Save ithinkdancan/ac70e03071623260279110e6cdb85c05 to your computer and use it in GitHub Desktop.
Server Rendering
import url from 'url';
import { get, includes, isNumber, omit } from 'lodash';
import { bindActionCreators } from 'redux';
import { matchPath } from 'react-router-dom';
import { flushChunkNames } from 'react-universal-component/server';
import flushChunks from 'webpack-flush-chunks';
import BodyClassName from 'react-body-classname';
import Helmet from 'react-helmet';
import uuidv4 from 'uuid/v4';
import createStore from 'store/create';
import { getDefaultPage } from 'store/modules/pages';
import { staticRoutes } from 'routes';
import { initialState as initialAppState, serverRenderError } from 'store/modules/app';
import { initialState as initialUserState } from 'store/modules/user';
import applicationRenderer from './lib/render/application';
import pageRenderer from './lib/render/page';
export default ({ clientStats, config, rollbar }) => (
(req, res, next) => {
const currentUrl = url.parse(req.originalUrl);
// Skip the health endpoint until it is created
if (includes(['/health'], currentUrl.pathname)) {
next();
return;
}
// Create the store
const store = createStore({
user: {
...initialUserState,
siteId: req.siteId,
visitId: req.query.SELENIUM ? 'SELENIUM' : (req.cookies.visit || req.visitId),
gcid: req.cookies.gcid,
user_ip: get(req, 'user_ip', null),
traffic_source_id: get(req, 'traffic_source_id', null),
cid: get(req, 'cid', null),
ctid: get(req, 'ctid', null)
},
app: {
...initialAppState,
ssag_host: config.ssag.external,
ssag_host_internal: config.ssag.internal,
baseUrl: config.webserver.baseUrl,
trackingURL: config.tracking.url,
trackingId: uuidv4(),
segmentWriteKey: config.segment.writeKey,
googleMapsKey: config.google.mapsKey,
recaptchaSiteKey: config.recaptcha.siteKey,
rollbarKey: config.rollbar.key,
environment: config.env
}
}, { rollbar, request: req });
// Find the matching route(s)
const branch = staticRoutes.reduce((branches, route) => {
const match = matchPath(currentUrl.pathname, {
path: route.path || '',
exact: true,
strict: true
});
return match ? [...branches, { route, match }] : branches;
}, []);
// Run through any matching routes to fetch their data
const routeData = branch
.filter(({ route }) => route.loadData)
.map(({ route, match }) => {
const actions = bindActionCreators(route.actions || {}, store.dispatch);
return route.loadData({
actions,
route,
match,
queryParams: req.query
});
});
// Get the default page data (Phone, Experiments)
const defaultPageData = store.dispatch(getDefaultPage());
const resolveData = Promise.all([defaultPageData, ...routeData]);
// Render the Application against the resolved data
const renderApplication = () => {
const state = store.getState();
// Render the application as a string
const { applicationString, redirectURL, statusCode } = applicationRenderer({
store,
location: currentUrl.path,
cookies: req.universalCookies
});
// Catch any application redirects before rendering
if (includes([301, 302], statusCode) && redirectURL) {
res.redirect(statusCode, redirectURL);
return;
}
// Return any status codes returned from the application
if (isNumber(statusCode)) {
res.status(statusCode);
}
// Get the rendered styles
const chunkNames = flushChunkNames();
const { styles, stylesheets, publicPath, scripts, js } = flushChunks(clientStats, { chunkNames });
// Render the page markup
const renderedPage = pageRenderer({
state: omit(state, 'app.ssag_host_internal'), // Application State with internal SSAG url removed
bodyClass: BodyClassName.rewind(), // Body classes applied during render
head: Helmet.renderStatic(), // Head Tags
applicationString, // Rendered React App
publicPath, // Client Asset Path
stylesheets, // Stylesheet Markup
styles, // Stylesheet List
scripts, // Scripts to preload
js // JS Tag Markup,
});
res.send(renderedPage);
};
resolveData
.then(renderApplication)
.catch((error) => {
// If an exception was thrown during the render, update the status and re-render
const errorMessage = config.env === 'production' ? error.message : error.stack;
store.dispatch(serverRenderError(errorMessage));
renderApplication();
rollbar.error(error, req);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment