Configuration-free SSR for Create React App
This is a fork of Create React App. For details on Create React App itself, see the official repository.
To create a new project with SSR, just use npm init
:
npm init universal-react-app my-ssr-app
Alternatively, you can use plain old create-react-app with a --react-scripts universal-react-scripts
option:
create-react-app my-ssr-app --scripts-version universal-react-scripts
Either way, you'll get the standard Create React App template with one extra file: src/index.node.js
(which is where you'll handle server side rendering).
In your package.json, just change the react-scripts
dependency to universal-react-scripts
. You can leave the occurences of react-scripts
in the scripts
object as is. Then, re-run yarn install
or npm install
, and add a src/index.node.js
file:
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) => {
// The index.html file is a template, which will have environment variables
// and bundled scripts and stylesheets injected during the build step, and
// placed at the location specified by `process.env.HTML_TEMPLATE_PATH`.
//
// To customize the rendered HTML, you can add other placeholder strings,
// and replace them within this function -- just as %RENDERED_CONTENT% is
// replaced. Note however that if you name the placeholder after an
// environment variable available at build time, then it will be
// automatically replaced by the build script.
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;
Your app will now call the function exported by src/index.node.js
to render its HTML.
Note: if you encounter any build errors after making this change, try deleting your node_modules
and reinstalling them from scratch.
Universal React Scripts outputs two versions of your app: a browser version, and a node version. The node version is just a common-js version of index.node.js
, which you can import in your express app or serverless function to handle server side rendering.
Universal React Scripts includes a serve
script for testing your build output:
npm run serve
# or
yarn serve
This launches a server for the content in the build
folder, and when a index.node.js
file exists, it loads the Node bundle and server the app with server rendering.
CURA can be used to do serverless SSR -- for example, using Firebase Functions. To get this working, you'll need to package up the files in build/node
and then call them within your serverless function.
See the example repository with routes, react-helmet and styled-components »
You can create a package with your app's commonjs code by setting the name
, files
and main
fields in your app's package.json
. Once the package is configured correctly, you'll be able to use npm pack
to create a tgz
file that can be added as a dependency to your functions package.json
-- which can be automated by adding a bash script as a predeploy
step.
Changes to package.json
:
{
"name": "app",
"main": "build/node/index.js",
"files": ["build/node/*.*"],
"scripts": {
"deploy": "firebase deploy",
"predeploy": "sh ./scripts/pack.sh"
}
}
scripts/pack.sh
:
rm functions/renderer.tgz
npm run build
mv "$(npm pack)" functions/renderer.tgz
functions/package.json
:
{
"optionalDependencies": {
"app": "./renderer.tgz"
}
}
The result of these three files is that each time you call yarn deploy
or npm run deploy
, a version of your app's index.node.js
file will be built, packaged, and then made available to your firebase functions. You can then import it and use it as so:
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp();
let renderer;
if (process.env.NODE_ENV === 'production') {
// Load the renderer from the packaged version when deploying
renderer = require('app').default;
} else {
// Load the renderer directly from the build directory during development.
// Note: the renderer is not used in development as CURA includes its own
// renderer, but the functions emulator fails without this.
renderer = require('../build/node').default;
}
exports.renderer = functions.https.onRequest(renderer);
Finally, if you're using Firebase Hosting, you'll need to configure it to route all non-static requests to the renderer
function. Here's what the resulting firebase.json
looks like:
{
"hosting": {
"public": "build/web",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"function": "renderer"
}
]
}
}
You can see an example of this setup, along with async routing, styled components and react-helmet, at the cura-firebase-example repository.
Learn more at the Create React App website.
If you'd like to understand the changes this fork makes to standard Create React App, see the Pull Request.
Create React App is open source software licensed as MIT.