React Server Side Rendering Boilerplate ⚛️

Tools like create-react-app have made setting up client-side React apps trivial, but transitioning to SSR is still kind of a pain in the ass. Next.js is a powerhouse, and the Razzle tool looks like an absolute beast, but sometimes you just want to see the whole enchilada running your app. This is a sample setup for fully featured, server-rendered React applications.

What's included:

  • Server-side rendering with code splitting (via the excellent React Loadable package)
  • Redux (with server-side data fetching/store population)
  • React Router
  • Conditionally load pollyfills -- only ship bloat to outdated browsers
  • React Helmet for dynamic manipulation of the document <head />
  • Dev server with hot reloading styles
  • Jest and Enzyme config ready to test the crap out of some stuff
  • CSS Modules, Sass, and autoprefixer
  • Run-time environment variables
  • Node.js clusters for improved performance under load (in production)
  • Prettier and ESLint run on commit
  • Docker-ized for production like a bawsss

Initial setup

  • npm install

Development

Production

  • npm run build && npm run start:prod
    • Bundle the JS and fire up the Express server for production
  • npm run docker
    • Build and start a local Docker image in production mode (mostly useful for debugging)

General architecture

This app has two main pieces: the server and the client code.

Server (server/)

A fairly basic Express application in server/app.js handles serving static assets (the generated CSS and JS code in build/ + anything in public/ like images and fonts), and sends all other requests to the React application via server/renderServerSideApp.js. That function delegates the fetching of server-side data pre-rendering to server/fetchDataForRender, and then sends the rendered React application (as a string) injected inside the HTML-ish code in server/indexHtml.js.

During development the server code is run with @babel/register and middleware is added to the Express app (see scripts/start), and in production we bundle the server code to build/server and the code in scripts/startProd is used to run the server with Node's cluster module to take advantage of multiple CPU cores.

Client (src/)

The entrypoint for the client-side code (src/index.js) first checks if the current browser needs to be polyfilled and then defers to src/main.js to hydrate the React application. These two files are only ever called on the client, so you can safely reference any browser APIs here without anything fancy. The rest of the client code is a React application -- in this case a super basic UI w/2 routes and a skeleton Redux setup, but you can safely modify/delete nearly everything inside src/ and make it your own. Note that if you do remove/replace react-router and redux you'll probably want to clean up server/renderServerSideApp.js as well.

As with all server-rendered React apps you'll want to be cautious of using browser APIs in your components -- they don't exist when rendering on the server and will throw errors unless you handle them gracefully (I've found some success with using if (typeof myBrowserAPI !== 'undefined') { ... } checks when necessary, but it feels dirty so I try to avoid when possible). The one exception to this is the componentDidMount() method for class components, this is only ever run on the client.

"How do I ...?"

Fetch data on the server and inject into the Redux store before rendering?

Sometimes you'll want to make API calls on the server to fetch data before rendering the page. In those cases you can add a static fetchDataForRender() method to the highest-level class component rendered for a route. That method will be called with the Redux store and the match object returned from react-router and has to return a Promise. Check out src/components/Home.js for an example. Eventually I'll add the ability to call fetchDataForRender() from any component being rendered on the server rather than only the top-level route component.

Current Quirks

  • DO NOT UPDATE TO webpack v4.29.4, it looks like there might be a problem between it and react-loadable.
  • This project does not have a webpack configuration that allows for the use of url-loader or file-loader (so no import src from 'my-img.svg'). Instead it relies on serving static assets via the public/ directory. See src/components/about/About.js for a reference on how to work with assets in your app see.
  • CSS modules are disabled for any files inside src/styles -- use this directory for global styles instead. This is set in the webpack config files, so start there if you'd like to change anything.
  • All routes should be defined in their normal react-router fashion. However, any routes that need to have data fetched before rendering (on the server) need some extra configuration inside sever/fetchDataForRender (in the ROUTES_THAT_FETCH_DATA array).

Roadmap

  • Run server via webpack in dev mode so we can use more loaders
  • Intelligently resolve CSS modules by looking for a .module.s?css file extension
  • Add example app that handles authentication
  • Migrate to react-testing-library instead of enzyme

cj-scripts

If you're interested in creating your own react-scripts-eque CLI that wraps up some of the concepts in this project take a look at my cj-scripts repo.