Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save designspin/c11095334ae1f105d1f93123232d37fd to your computer and use it in GitHub Desktop.
Save designspin/c11095334ae1f105d1f93123232d37fd to your computer and use it in GitHub Desktop.
Styled-Components with React Universally
import React from 'react';
import Helmet from 'react-helmet';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { AsyncComponentProvider, createAsyncContext } from 'react-async-component';
import asyncBootstrapper from 'react-async-bootstrapper';
import config from '../../../config';
import ServerHTML from './ServerHTML';
import DesignSpin from '../../../shared/components/DesignSpin';
import { ServerStyleSheet } from 'styled-components';
/**
* React application middleware, supports server side rendering.
*/
export default function reactApplicationMiddleware(request, response) {
// Ensure a nonce has been provided to us.
// See the server/middleware/security.js for more info.
if (typeof response.locals.nonce !== 'string') {
throw new Error('A "nonce" value has not been attached to the response');
}
const nonce = response.locals.nonce;
// It's possible to disable SSR, which can be useful in development mode.
// In this case traditional client side only rendering will occur.
if (config('disableSSR')) {
if (process.env.BUILD_FLAG_IS_DEV === 'true') {
// eslint-disable-next-line no-console
console.log('==> Handling react route without SSR');
}
// SSR is disabled so we will return an "empty" html page and
// rely on the client to initialize and render the react application.
const html = renderToStaticMarkup(<ServerHTML nonce={nonce} />);
response.status(200).send(`<!DOCTYPE html>${html}`);
return;
}
// Create a context for our AsyncComponentProvider.
const asyncComponentsContext = createAsyncContext();
// Create a context for <StaticRouter>, which will allow us to
// query for the results of the render.
const reactRouterContext = {};
// Declare our React application.
const app = (
<AsyncComponentProvider asyncContext={asyncComponentsContext}>
<StaticRouter location={request.url} context={reactRouterContext}>
<DesignSpin />
</StaticRouter>
</AsyncComponentProvider>
);
// Pass our app into the react-async-component helper so that any async
// components are resolved for the render.
asyncBootstrapper(app).then(() => {
const appString = renderToString(app);
// Generate the html response.
const html = renderToStaticMarkup(
<ServerHTML
reactAppString={appString}
nonce={nonce}
helmet={Helmet.rewind()}
asyncComponentsState={asyncComponentsContext.getState()}
sheet={new ServerStyleSheet()}
/>,
);
// Check if the router context contains a redirect, if so we need to set
// the specific status and redirect header and end the response.
if (reactRouterContext.url) {
response.status(302).setHeader('Location', reactRouterContext.url);
response.end();
return;
}
response
.status(
reactRouterContext.missed
? // If the renderResult contains a "missed" match then we set a 404 code.
// Our App component will handle the rendering of an Error404 view.
404
: // Otherwise everything is all good and we send a 200 OK status.
200,
)
.send(`<!DOCTYPE html>${html}`);
});
}
/**
* This module is responsible for generating the HTML page response for
* the react application middleware.
*/
/* eslint-disable react/no-danger */
/* eslint-disable react/no-array-index-key */
import React, { Children } from 'react';
import PropTypes from 'prop-types';
import serialize from 'serialize-javascript';
import config from '../../../config';
import ifElse from '../../../shared/utils/logic/ifElse';
import removeNil from '../../../shared/utils/arrays/removeNil';
import getClientBundleEntryAssets from './getClientBundleEntryAssets';
import ClientConfig from '../../../config/components/ClientConfig';
import HTML from '../../../shared/components/HTML';
// PRIVATES
function KeyedComponent({ children }) {
return Children.only(children);
}
// Resolve the assets (js/css) for the client bundle's entry chunk.
const clientEntryAssets = getClientBundleEntryAssets();
function stylesheetTag(stylesheetFilePath) {
return (
<link href={stylesheetFilePath} media="screen, projection" rel="stylesheet" type="text/css" />
);
}
function scriptTag(jsFilePath) {
return <script type="text/javascript" src={jsFilePath} />;
}
// COMPONENT
function ServerHTML(props) {
const { asyncComponentsState, helmet, nonce, reactAppString, sheet } = props;
// Creates an inline script definition that is protected by the nonce.
const inlineScript = body =>
<script nonce={nonce} type="text/javascript" dangerouslySetInnerHTML={{ __html: body }} />;
const css = sheet.getStyleElement();
const headerElements = removeNil([
...ifElse(helmet)(() => helmet.title.toComponent(), []),
...ifElse(helmet)(() => helmet.base.toComponent(), []),
...ifElse(helmet)(() => helmet.meta.toComponent(), []),
...ifElse(helmet)(() => helmet.link.toComponent(), []),
ifElse(clientEntryAssets && clientEntryAssets.css)(() => stylesheetTag(clientEntryAssets.css)),
...ifElse(helmet)(() => helmet.style.toComponent(), []),
...ifElse(css.length > 0)(() => css, []),
]);
const bodyElements = removeNil([
// Binds the client configuration object to the window object so
// that we can safely expose some configuration values to the
// client bundle that gets executed in the browser.
<ClientConfig nonce={nonce} />,
// Bind our async components state so the client knows which ones
// to initialise so that the checksum matches the server response.
// @see https://github.com/ctrlplusb/react-async-component
ifElse(asyncComponentsState)(() =>
inlineScript(
`window.__ASYNC_COMPONENTS_REHYDRATE_STATE__=${serialize(asyncComponentsState)};`,
),
),
// Enable the polyfill io script?
// This can't be configured within a react-helmet component as we
// may need the polyfill's before our client JS gets parsed.
ifElse(config('polyfillIO.enabled'))(() =>
scriptTag(`${config('polyfillIO.url')}?features=${config('polyfillIO.features').join(',')}`),
),
// When we are in development mode our development server will
// generate a vendor DLL in order to dramatically reduce our
// compilation times. Therefore we need to inject the path to the
// vendor dll bundle below.
ifElse(
process.env.BUILD_FLAG_IS_DEV === 'true' && config('bundles.client.devVendorDLL.enabled'),
)(() =>
scriptTag(
`${config('bundles.client.webPath')}${config(
'bundles.client.devVendorDLL.name',
)}.js?t=${Date.now()}`,
),
),
ifElse(clientEntryAssets && clientEntryAssets.js)(() => scriptTag(clientEntryAssets.js)),
...ifElse(helmet)(() => helmet.script.toComponent(), []),
]);
return (
<HTML
htmlAttributes={ifElse(helmet)(() => helmet.htmlAttributes.toComponent(), null)}
headerElements={headerElements.map((x, idx) =>
(<KeyedComponent key={idx}>
{x}
</KeyedComponent>),
)}
bodyElements={bodyElements.map((x, idx) =>
(<KeyedComponent key={idx}>
{x}
</KeyedComponent>),
)}
appBodyString={reactAppString}
/>
);
}
ServerHTML.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
asyncComponentsState: PropTypes.object,
// eslint-disable-next-line react/forbid-prop-types
helmet: PropTypes.object,
nonce: PropTypes.string,
reactAppString: PropTypes.string,
};
// EXPORT
export default ServerHTML;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment