Server rendering
ssr-react
In this guide you'll learn about using done-ssr to server render a React application. This guide walks through the technologies that make DoneJS server rendering special:
- Using Zones to isolate rendering, allowing multiple requests to be served from the same application
- Server-side virtual DOM with common APIs, allowing you to use the same code that runs on the client, for rendering on the server
- HTTP/2 support, and H/2 PUSH, to PUSH out API requests as they are fulfilled on the server
- Incremental rendering using HTTP/2 PUSH and the fetch API.
Setting up
This app uses create-react-app to scaffold a React application and for client development. We'll be swapping out the server for our own, to enable server-side rendering.
If you haven't already, install create-react-app:
Then, create the application:
This will install the dependencies to a new folder
my-react-app/
. Once done you can cd into the folder and start the development server:This will start a development server and launch the browser. It looks like:
Adding a server
In order to use done-ssr for server-side rendering, we need to first set up a server. We'll use Express as our server framework. Install a few dependencies we need:
Create a folder to put our server code:
Create
server/index.js
:To run it, you need to add a
NODE_ENV
environment variable. For convenience add this to your package.json scripts:Then run the build step for your React app:
And then launch it with:
And navigate to http://localhost:3000.
Let's break down the interesting parts of this server.
React
can-zone-jsdom is the project that we use to provide a DOM environment for the app. This plugin allows you to load an HTML file for each request.
This server directly uses the entry point
build/index.html
in the Node application. This is an HTML file that is created whennpm run build
is ran. You can see the use here:This says that the document.baseURI is the build folder, and to use index.html to load into the DOM.
For each request the HTML file is loaded and its scripts are executed.
Routing
We are using Express to provide routing for this application. In addition, it handles server static files:
There is only one route defined:
This means that the server will first check for a static file in either
build/
or.
, and if not available, it will go to the wildcard route.This allows us to handle routing for our app the same way as we would in the browser. We are only handling the
/
route in this application, but there are many choices for routing in React such as React-Route or page.js.Zones
The rest of this guide will focus on the code contained within the
*
route. This uses can-zone to act as a common context when calling into the client-side components (starting withsrc/App.js
). You can read more about can-zone here.done-ssr provides a set of zone plugins (referred to hereafter as zones) that provide various capabilities. Right now we are using only 2 zones:
XMLHttpRequest
,fetch
, andWebSocket
./api/todos
.document
,window
, andlocation
objects. Serializes the document (when the zone is complete) aszone.data.html
.Later in the guide we'll add a couple of more, for HTTP/2 support.
Breaking down the steps here, first we have:
This creates a new can-zone using the previously mentioned zone plugins.
Runs the contents of
index.html
within the zone and waits for it to asynchronously complete. Once completed extracts thehtml
string and ends theresponse
with that as the value.Remember that the index.html is run every time a request is rendered, and a new
document
is set for each request (by can-zone-jsdom).HTTP/2
Using done-ssr makes it very simple to support HTTP/1 applications, but we can do even better using HTTP/2 and incremental rendering.
Using the done-ssr/zones/push-mutations zone, we can add incremental rendering to this application.
Setup
First, we need to install an HTTP/2 server. While HTTP/2 support is in Node 8.6.0 behind a flag, I've found it to be too buggy to use today. So use donejs-spdy instead:
At this point you'll need to create a private key and certificate, as HTTP/2 requires SSL. If using a Unix operating system, you can use openssl for this:
This will create server.key and server.cert files. I like to copy those to another folder so that they can be reused in other applications.
Lastly, update your package.json so these files are available to use:
Update server
Now that you have SSL and an HTTP/2 server installed, update your
server/index.js
script to:This adds two new zones to our arsenal:
done-ssr/zones/push-fetch: Traps calls to the fetch API and uses H2 PUSH to start pushing them to the browser. This way when the browser JavaScript tries to fetch this resource it's already available in the browser cache.
done-ssr/zones/push-mutations: This is the zone that handles incremental rendering. It does a few things interesting:
<head>
that will attach to a special URL where the mutations are being streamed. When this script runs in the browser it will fetch that URL and start applying the mutation patches as they come in.If you start your server again with
npm run server
, you should be able to see the application running.Adding an API
Even though we have incremental server-side rendering set up, since we're not doing any
fetch
requests, there are no mutations to be applied. So let's add an API route and have our client code make a request.List component
To make this even better, we'll use ReadableStream, the advanced feature of
fetch
that allows you to stream in the request in chunks. When streaming in API requests, it's good to use the ndjson format.ndjson is just JSON that is separated by newline characters. It looks like this:
We'll use can-ndjson-stream to make it easier to work with this format. So install that first:
We'll use this in our client code. Create
src/List.js
:And then use it within src/App.js:
What is happening in src/List.js:
items
Array in the state./api/items
.can-ndjson-stream
is called with the response body.this.state.items
.render()
is recalled and a new<li>
is created for each item.API route
Now that we have the client code we need to set up the route to handle it. Create server/api.js with the following:
This route is very simple, it returns an ndjson stream that emits a row every 500 milliseconds. Since there are 10 rows it takes 5 seconds for this to complete. This is just enough time to see incremental rendering in action.
To use it, update server/index.js:
And then run the build:
Now, if you restart your server you should see the list incrementally updating.
Conclusion
In this guide we've discussed:
To see this guide as a fully working example, check out the done-ssr-react-example repository.