Created by: jamesknelson
This adds optional SSR support for create-react-app.
It takes a similar approach to TypeScript support -- the existence of a src/index.node.js
file enables SSR support. When src/index.node.js
does not exist, the behavior does not change compared to the current master branch.
I haven't yet added docs/tests, but you can try it out following the instructions below. If there's any chance of this being merged, I'm happy to add tests/docs -- but I'd like to see some discussion before doing the work.
This PR does not attempt to solve routing or code splitting, as streaming SSR should eventually remove the need for anything extra for handling these, and it's still possible to handle these right now using things like Apollo, react-ssr-prepass or Navi.
How to try it
# first clone the branch, then
yarn
yarn create-react-app ssrtest --universal # also can stack with --typescript
cd ssrtest
yarn start # view source to see that it's server rendered
yarn build
yarn serve # starts a server that loads the node bundle
Alternatively, I've published a version of this to npm as universal-react-scripts
, so you can try it out using the standard create-react-app
script:
npm init react-app my-ssr-app --scripts-version=universal-react-scripts
The node bundle
When src/index.node.js
does exist, a Node version of the bundle will be built alongside the Web version, using src/index.node.js
as the entry point instead of src/index.js
. Here's my proposed default:
import fs from 'fs';
import React from 'react';
import { renderToString } from 'react-dom/server';
import './index.css';
import App from './App';
const renderer = async (request, response) => {
// This environment variable points to the location of the *output* of the HTML file
// generated by webpack, including <script> and <link> tags. It's location will change
// between development and production.
let template = fs.readFileSync(process.env.HTML_TEMPLATE_PATH, 'utf8');
let [header, footer] = template.split('%RENDERED_CONTENT%');
let body = renderToString(<App />);
let html = header+body+footer;
response.send(html);
}
export default renderer;
When this file exists, two bundles of the app will be built: one in build/web
, and one in build/node
. The version in build/node
is a commonjs module, so you can do anything with it, but the above default has been chose to be easily usable from Express, and from Lambda function on services such as ZEIT Now and Firebase. It also allows for SSR during development, using the dev middleware.
The dev middleware
This adds a new middleware that is enabled when src/index.node.js
exists. This middleware loads the default export of src/index.node.js
, and uses it to render each page during development -- ensuring that server rendering "just works" during development. This should cause no user-facing changes at all.
Other changes
- While there was already support for
.web.js
extensions, I've added support for.node.js
extensions as well, which are given preference for the node build. - In order to allow
process.env
to be used within the Node files, I've modified env.js to pass through any existing value of process.env. - A
--universal
option is passed through fromcreate-react-app
to the init script. - The init script uses existing templates, but also looks for a
[templatename]-universal
template which can be used to override specific files. This is used to add thesrc/index.node.js
orsrc/index.node.tsx
file when--universal
is enabled, and to switch toReactDOM.hydrate()
instead ofReactDOM.render()
.