Created
July 13, 2017 21:38
-
-
Save designspin/c11095334ae1f105d1f93123232d37fd to your computer and use it in GitHub Desktop.
Styled-Components with React Universally
This file contains 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 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 file contains 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
/** | |
* 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