🖍 Combining React Client and Render Server for SSR

Photo by Mike Petrucci on Unsplash

This is part 6 of my series on server-side rendering (SSR):

  1. 🤷🏻‍♂️ What is server-side rendering (SSR)?
  2. ✨ Creating A React App
  3. 🎨 Architecting a privacy-aware render server
  4. 🏗 Creating An Express Server
  5. 🖥 Our first server-side render
  6. [You are here] 🖍 Combining React Client and Render Server for SSR
  7. ⚡️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. 💧 Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. 🛑 React Hydration Error Indicator
  11. 🧑🏾‍🎨 Render Gateway: A Multi-use Render Server

Here we are at last? Finally, a server-side rendered application? We shall see1. After making a simple client-side React application and talking about, building, and extending a server-side rendering (SSR) architecture, we can do the work of combining the two.

Last time we took our simple express server and got it to return an HTML page that included some actual rendered React. It was not particularly flashy, but it did prove out the concept. So, what now? Well, we need to somehow have our server-side code render our client-side code.

🥣 Combining the server and client

Currently, these are two separate projects. One, our client-side app, is using JSX; the other, our server-side app, does not know what JSX is. To combine them, we need to either teach the server about JSX or incorporate a built version of the client app.

My personal preference is to keep the server as unaware of the client-side specifics as possible. This has the advantage that in the long term, the server might even need to know that it is rendering React at all. However, before we resort to that, let's try to compromise a little for the sake of keeping things simple.

Incorporating our client app into our server app is not quite so simple as just copying the code over. Our server needs to understand it. Thankfully, it only needs to understand how to turn JSX into regular JS, so we can add our own Babel configuration for that. Since the client app is in its own Git repository, we can import a version of it via commit SHA. However, we do need the production build of the template, which creates a problem since we cannot build the client application in the server repository; it has developer dependencies that won't be fulfilled in the server project.

The traditional way to resolve that would be to publish the client app as a package in a package repository. That way the built template would be a part of the package for us to include. Of course, we are not going to do that; it's far more work than I want to do right now. Instead, what we will do is have both packages co-exist in the same repository as siblings. The server can then easily invoke commands on the client application if it so requires in order to ensure access to the client production assets. This allows both server and client to exist on their own while still allowing for the integration we seek2.

To get the server able to compile the JSX files, I ran the following commands, added the given Babel config file, and modified our package.json scripts to add babel-watch to the start command so that we can edit code and auto-transpile and restart3. With these changes, I then updated the index.js server file to replace our previous hack of using React.createElement with some actual JSX.

yarn add --dev @babel/cli @babel/core @babel/proposal-plugin-class-properties @babel/preset-env @babel/preset-react
{
  "name": "hello-react-world-ssr",
  "version": "0.0.1",
  "description": "A server-side rendering server",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "express": "^4.17.1",
    "react": "^16.12.0",
    "react-dom": "^16.12.0"
  },
  "scripts": {
    "start": "NODE_ENV=development babel-watch index.js"
  },
  "devDependencies": {
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.4",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/preset-env": "^7.8.4",
    "@babel/preset-react": "^7.8.3",
    "babel-watch": "^7.0.0"
  }
}
const express = require("express");
const React = require("react");
const {renderToString} = require("react-dom/server");
const {getPageTemplate} = require("./get-page-template.js");

const port = 3000;
const app = express();

const pageTemplate = getPageTemplate();

const renderPage = (reactComponent) => {
    const renderedComponent = renderToString(reactComponent);
    return pageTemplate.replace('<div id="root"></div>', `<div id="root">${renderedComponent}</div>`);
};

app.get("/*", (req, res) => res.send(
    renderPage(<div>Hello World!</div>),
));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Running yarn start demonstrates that this updated code is working, so our final step is to incorporate our actual App component. This is as simple as importing the App component and then replacing the <div>Hello World!</div> with <App />. Great! yarn start again.

Sadly, this immediately results in an error when the App component tries to import the SVG logo. To work around this, we can extend Node's require to support the SVG extension (while we're at it, we also have to do this for the CSS imports).

const fs = require("fs");

const requireText = function (module, filename) {
    module.exports = fs.readFileSync(filename, 'utf8');
};

require.extensions[".svg"] = requireText
require.extensions[".css"] = requireText;

Now when we yarn start, things appear to be all right. However, if we navigate to localhost:3000, instead of any part of our app, we get an error:

Error: Invariant failed: Browser history needs a DOM
    at invariant (/hello-react-world/client/node_modules/tiny-invariant/dist/tiny-invariant.cjs.js:13:11)
    at Object.createBrowserHistory (/hello-react-world/client/node_modules/history/cjs/history.js:273:16)
    at new BrowserRouter (/hello-react-world/client/node_modules/react-router-dom/modules/BrowserRouter.js:11:13)
    at processChild (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3159:14)
    at resolve (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3124:5)
    at ReactDOMServerRenderer.render (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3598:22)
    at ReactDOMServerRenderer.read (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:3536:29)
    at renderToString (/hello-react-world/server/node_modules/react-dom/cjs/react-dom-server.node.development.js:4245:27)
    at renderPage (/hello-react-world/server/index.js:14:31)
    at app.get (/hello-react-world/server/index.js:19:5)

Oh noes! Indeed, we forgot that our app currently renders inside a BrowserRouter component and we don't have a browser with history on our render server. Of course, we could fake one, but there is no need as React Router already has us covered with StaticRouter. However, in order to use that, we're going to need to change our client-side component a little to accommodate our server expectations.

Next time, we will introduce the StaticRouter, and see that it leads to more things that we must address to get our application working the way we want. Until then, please question, comment, and discuss. The aim of this whole series is to learn more about how we can leverage SSR. That means we will continue to get messy with doing things the hard way and build an appreciation for the easy way we hopefully uncover. 🕵🏻‍♂️

  1. Hint: the answer is "sort of". []
  2. Knowledgeable folks among you will already see problems with this approach; we shall get to those []
  3. Eventually, we may want to build our server so that we do not need babel-watch for production usage; for now, it is not important. []

🖥 Our first server-side render

Photo by Markus Spiske on Unsplash

This is part 5 of my series on server-side rendering (SSR):

  1. 🤷🏻‍♂️ What is server-side rendering (SSR)?
  2. ✨ Creating A React App
  3. 🎨 Architecting a privacy-aware render server
  4. 🏗 Creating An Express Server
  5. [You are here] 🖥 Our first server-side render
  6. 🖍 Combining React Client and Render Server for SSR
  7. ⚡️ Static Router, Static Assets, Serving A Server-side Rendered Site
  8. 💧 Hydration and Server-side Rendering
  9. 🦟 Debugging and fixing hydration issues
  10. 🛑 React Hydration Error Indicator
  11. 🧑🏾‍🎨 Render Gateway: A Multi-use Render Server

Over the last few weeks, we have been chipping away at server-side rendering and how to implement it. In the last post, we created a server; in this one, we will see if we can make that server render a page containing some server-side rendered React. If you recall from last time, there are two ways we can approach implementing our render server:

  1. Standalone with a way to pass our React app to it
  2. Integrated so that it knows all about our React app

Both approaches start from common origins; they both need a server that can render a React component inside a page. By the end of this post, we should be able to request a URL from our server and receive a rendered HTML page with some rendered React embedded inside of it. This will give us some fundamentals that we can then use next time to finally render our client-side application.


🖌 Rendering React on the server

app.get("/*", (req, res) => res.send("Hello World!"));

The server that we made last time will be the basis for our solution. Above is our current route handler for the server. Regardless of the get request, the server responds with Hello World!. What we want to do is to replace Hello World! with the rendered React component tree embedded within an HTML page.

When rendering in a browser, our React application is mounted so that it will dynamically update based on events like mouse movements, network requests, etc. When we are rendering on the server, we do not want all that. In fact, we do not even have a DOM like the browser does in which to create elements and event handlers and the like1. Instead of mounting the React application, we want to capture the very first render of the application and stop. React provides a methods for doing things like this in the React DOM package. We are going to use its renderToString method2.

The renderToString method takes a React component and gives us back a string of the markup that is initially rendered by that component. Before we can try it, we need to add the appropriate packages to our server: react and react-dom.

yarn add react react-dom

Now, in theory, we can render some React. Sadly, just updating our route handler with a component as shown below will not work.

app.get("/*", (req, res) => res.send(
    renderToString(<div>Hello World!</div>)
));

If we add this and then run our server with yarn start, we get a rather cryptic error output.

app.get("/*", (req, res) => res.send(renderToString(<div>Hello World!</div>)));
                                                    ^

SyntaxError: Unexpected token <
    at Module._compile (internal/modules/cjs/loader.js:723:23)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)

The issue is, our server has no idea how to handle JSX syntax (the embedded HTML-like description of our React component; <div>Hello World!</div>). Our client-side application works because the create-react-app package sets up some tools to process JSX files and turn them into valid JavaScript that can be understood by a modern browser. Our server-side application does not have any of that magic and so it does not work.

Rather than spending time to add that magic into our server, it feels more appropriate to assume our server is just another browser and that our client-side code will already be transformed into JavaScript by the time we see it3. Instead, just to test our React rendering, we can replace the JSX with its transpiled counterpart, which is a call to React.createElement. React.createElement takes the component being rendered and its props. In our case, we are rendering an HTML div element. These are special cases where a string is used to represent them, rather than a real React component type. Therefore, our simple JSX example becomes; note how the text is passed as the children of the component.

app.get("/*", (req, res) => res.send(
    renderToString(React.createElement("div", { children: "Hello World!" }))),
);

If we now yarn start our server application, it runs and when we visit http://localhost:3000, we see our Hello World! text. This is great. It means that given a suitably transpiled React component, we can server-side render it. Now that we have the ability to render a component, we need to embed that rendered component inside a full HTML page.

📄 The Page Template

OK, so we have some HTML that represents our rendered component. Now we need to put that into an HTML page, we need to think about what that page looks like. What does the page include? We can revisit the React app we made and see for ourselves.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="/manifest.json" />
    <!--
      Notice the use of  in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  <script src="/static/js/bundle.js"></script><script src="/static/js/0.chunk.js"></script><script src="/static/js/main.chunk.js"></script></body>
</html>

Above is the development-time HTML template that is used with our simple React app. I have highlighted some important sections.

  1. The head containing page metadata, including title, description, favicon, etc.
  2. Scaffold body to provide a mounting point for our React component
  3. Scripts

The head content is static4 and the scripts are inserted by the build operation of our client app. The bit that matters to us is the mounting point for our React app, <div id="root"></div>, as this is where anything we render will need to be inserted by our server-side rendering operation.

Of course, we want to do all this with production code. To see how that affects things, we can run yarn build in our React app. Running this creates a build folder with all sorts of things in it, including a slightly different version of our HTML template (we will perhaps consider the other files another time).

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
    <link href="/static/css/main.b0083702.chunk.css" rel="stylesheet" />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script>
      !(function(f) {
           // SNIPPED FOR CLARITY
      })([]);
    </script>
    <script src="/static/js/2.78e6b881.chunk.js"></script>
    <script src="/static/js/main.dcbf6a7c.chunk.js"></script>
  </body>
</html>

This looks a little different than what we had before, but it is not as different as you may think. We still have the head element metadata (though this time it includes a CSS file, which was not there in the development version), we still have the scripts (though there is now some inlined scripting that wasn't there before, which I have snipped out just to make things a little more readable), and most importantly, we still have our mounting point, <div id="root"></div>.

Given this information, we can update our server application to return a full page containing our rendered component. For our purposes here, we will hard code a simple page template. Eventually, we can replace this simple template and the React component being rendered with the production output of our client application.

🖼 Rendering the page and the component together

Using what we have learned here, I have modified the server as follows.

const express = require("express");
const React = require("react");
const {renderToString} = require("react-dom/server");

const port = 3000;
const app = express();

const pageTemplate = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="description"
      content="SSR result"
    />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
`;

const renderPage = (reactComponent) => {
    const renderedComponent = renderToString(reactComponent);
    return pageTemplate.replace('<div id="root"></div>', `<div id="root">${renderedComponent}</div>`);
};

app.get("/*", (req, res) => res.send(
    renderPage(React.createElement("div", {children: "Hello World!"})),
));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

We have a page template string called pageTemplate. Then we have a renderPage method that does a simple replace operation to replace <div id="root"></div> in our template with the same div containing our rendered React component. Finally, in the get handler, the renderPage method is invoked with our React component.

If we yarn start this version of the server and visit http://localhost:3000, viewing the resultant page source gives us the following HTML.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="description"
      content="SSR result"
    />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"><div data-reactroot="">Hello World!</div></div>
  </body>
</html>

On the highlighted line, you can see the inserted server-side rendered React code. Success! We haven't even loaded any scripts client-side to see this result. Of course, if we want an app that a user can interact with, we are going to need to change that. Join me next time when we work out how to integrate our server with our client-side application in order to get our very first server-side rendered app. Of course, that does not mean we will have reached our destination on this server-side rendering adventure; on the contrary, it feels like we have barely begun.

Thanks again for reading. I hope that something you find here is useful. Please comment as you see fit. 💝

  1. We could introduce a DOM using a library like JSDOM. However, that is an extra step that we would like to avoid as it increases the latency of our server-side rendering process. Instead, we should aim to make sure our React app does not rely on there being a DOM present at all. []
  2. It is worth looking at the other options, such as renderToNodeStream, to see what they can offer you and your specific SSR challenges []
  3. You may notice that this starts to lead us down one of our two paths; does the server know all about the client code, or does it get blindly provided somehow? []
  4. For now, let's assume that the page metadata (item 1, above) remains static; while we can certainly build in a mechanisms to make this dynamic, such as changing page title when the selected route is different, that will over-complicate things at this stage. []