⚡️ Static Router, Static Assets, Serving A Server-side Rendered Site

Featured image from photo by Greg Jeanneau on Unsplash

This is part 7 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. 🖍 Combining React Client and Render Server for SSR
  7. [You are here] ⚡️ 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

Last time, we worked to combine the client website implementation and the server rendering architecture into a single solution. Unfortunately, where we ended up was a failing app. This week we are going to jump straight in where we left off with the goal of finally server-side rendering our website. I'll let you in on a little secret; we hit that goal this week1!

🗺 StaticRouter

At the end of our attempts to integrate our website with the server, we got this error:

Error: Invariant failed: Browser history needs a DOM

To fix this, our first instinct might be to provide a DOM using perhaps JSDOM. However, this additional dependency is not the right way to resolve this error. Instead, we can look to React Router. It turns out that besides the BrowserRouter, React Router provides other router implementations such as HashRouter, MemoryRouter, and StaticRouter. There are different scenarios where these different routers come into play – for example, the MemoryRouter is really useful for testing, including storyboard style tests where one might want to verify that navigation would happen without actually having it happen. In the case of server-side rendering, it is the StaticRouter component that is there to help us. StaticRouter is pretty much what it says. It provides all the things React Router's infrastructure needs to render routes but with none of the navigation support. We tell it the location we are at and it ensures that is the route we render.

To get StaticRouter into our app we have now come to the point where our client-side website implementation can no longer be oblivious to our server-side rendering implementation. As we shall find during our SSR adventures, this will not be the only time our client app must consider its server-side rendering, but we can make it as seamless as possible. To start, let's modify our root App component so that we can control whether it renders a BrowserRouter or a StaticRouter.

import React from 'react';
import {BrowserRouter, StaticRouter} from "react-router-dom";

export default function Router(props) {
    const {ssrLocation, children} = props;

    if (ssrLocation == null) {
        return <BrowserRouter>{children}</BrowserRouter>;
    }

    return <StaticRouter location={ssrLocation}>{children}</StaticRouter>;
}
import React from 'react';
import {Link, Route, Switch} from "react-router-dom";
import Router from "./Router.js";
import logo from './logo.svg';
import './App.css';

function App(props) {
  const {ssrLocation} = props;
  return (
    <Router ssrLocation={ssrLocation}>
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <div className="App-links">
            <Link className="App-link" to="/">Home</Link>
            <Link className="App-link" to="/about">About</Link>
            <Link className="App-link" to="/contact">Contact</Link>
          </div>
        </header>
        <section className="App-content">
          <Switch>
            <Route path="/about">
              This is the about page!
            </Route>
            <Route path="/contact">
              This is the contact page!
            </Route>
            <Route path="/">
              This is the home page!
            </Route>
          </Switch>
        </section>
      </div>
    </Router>
  );
}

export default App;
require("./require-patches.js");
const express = require("express");
const React = require("react");
const {renderToString} = require("react-dom/server");
const {getPageTemplate} = require("./get-page-template.js");
const App = require("../client/src/App.js").default;

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(<App ssrLocation={req.url}  />),
));

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

The first thing I did was to create a new component in our client app called Router. This component encapsulates the choice between using BrowserRouter or StaticRouter. It takes two props: children and ssrLocation. The first, children is what the component will render as its children (this is a React idiom – to use the prop children for any child components that a component will render). The second, ssrLocation is the URL that will be used with the StaticRouter when server-side rendering. If ssrLocation is not specified, we assume we are client-side and use BrowserRouter; if ssrLocation is specified, we assume we are server-side and use ServerRouter.

Second, I updated our App component to use this new Router component and to allow for the provision of an ssrLocation prop. I have highlighted the more important lines of that change.

With the client-side app now smart enough to handle the two worlds, we need the server to provide the ssrLocation prop. This change is highlighted in the server/index.js.

If we run the server with yarn start and visit localhost:3000 to see if it worked.

Screenshot from browser showing the URL of localhost:3000, and a page with an image placeholder called "logo", three links without padding nor margins labelled "Home", "About", and "Contact", and the text "This is the home page!"

Success-ish! We can even visit a different route like localhost:3000/about and see that it renders the about page. Brilliant, even though there is no nice spinning SVG and no styles, we rendered our app without an obvious error. Still, where is that logo and where are out styles? If we look at the server's console we see a couple of errors indicating that some files were requested but we failed to return them because we failed to decode the URL. Are these errors a clue to our missing CSS?

yarn run v1.21.1
$ NODE_ENV=development babel-watch index.js
Example app listening on port 3000!
URIError: Failed to decode param '%PUBLIC_URL%/manifest.json'
    at decodeURIComponent (<anonymous>)
    at decode_param (/hello-react-world/server/node_modules/express/lib/router/layer.js:172:12)
    at Layer.match (/hello-react-world/server/node_modules/express/lib/router/layer.js:148:15)
    at matchLayer (/hello-react-world/server/node_modules/express/lib/router/index.js:574:18)
    at next (/hello-react-world/server/node_modules/express/lib/router/index.js:220:15)
    at expressInit (/hello-react-world/server/node_modules/express/lib/middleware/init.js:40:5)
    at Layer.handle [as handle_request] (/hello-react-world/server/node_modules/express/lib/router/layer.js:95:5)
    at trim_prefix (/hello-react-world/server/node_modules/express/lib/router/index.js:317:13)
    at /hello-react-world/server/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/hello-react-world/server/node_modules/express/lib/router/index.js:335:12)
URIError: Failed to decode param '%PUBLIC_URL%/favicon.ico'
    at decodeURIComponent (<anonymous>)
    at decode_param (/hello-react-world/server/node_modules/express/lib/router/layer.js:172:12)
    at Layer.match (/hello-react-world/server/node_modules/express/lib/router/layer.js:148:15)
    at matchLayer (/hello-react-world/server/node_modules/express/lib/router/index.js:574:18)
    at next (/hello-react-world/server/node_modules/express/lib/router/index.js:220:15)
    at expressInit (/hello-react-world/server/node_modules/express/lib/middleware/init.js:40:5)
    at Layer.handle [as handle_request] (/hello-react-world/server/node_modules/express/lib/router/layer.js:95:5)
    at trim_prefix (/hello-react-world/server/node_modules/express/lib/router/index.js:317:13)
    at /hello-react-world/server/node_modules/express/lib/router/index.js:284:7
    at Function.process_params (/hello-react-world/server/node_modules/express/lib/router/index.js:335:12)

If we run the server in production mode with yarn start:prod and revisit localhost:3000, we see the same HTML rendered with no logo nor CSS and this time, there are no server console errors. However, if we look in the browser console, we see some curious errors:

2.dbb51963.chunk.js:1 Uncaught SyntaxError: Unexpected token '<'
main.448e42f3.chunk.js:1 Uncaught SyntaxError: Unexpected token '<'
manifest.json:1 Manifest: Line: 1, column: 1, Syntax error.

If we select the Network tab and refresh, then click on the response for one of these files, we can see exactly what is happening to cause the console errors, and see a clue as to why our logo is missing.

Our server is responding to every request as if it were the request for a page, when we really want it to return the files that were requested. In addition, we can see that instead of being written with tags, the SVG has been written out with HTML entities. These are two different problems. Let's fix the SVG one first.

🎩 Repetitive Magic

If you recall, last time around we taught NodeJS how to import SVG and CSS files with custom import extensions that looked like this:

const fs = require("fs");
const requireText = function (module, filename) {
    module.exports = fs.readFileSync(filename, 'utf8');
};
require.extensions[".svg"] = requireText
require.extensions[".css"] = requireText;

This is then used in our App component as follows (I have pared this down to just the import and usage for clarity):

import logo from './logo.svg';

<img src={logo} className="App-logo" alt="logo" />

That is not right. Of course our logo is not showing up because we're producing a nonsense source string. To see why our client app does not have this issue, let's run it in dev mode and see how that img tag gets rendered.

<img src="/static/media/logo.5d5d9eef.svg" class="App-logo" alt="logo">

That is weird. That file path does not even exist. It turns out webpack is doing some magic work for us to ensure that the import of the SVG file becomes an actual URL to the static file on disk rather than some nonsense inline pseudo-HTML like we have from our server. So, what do we do?

Well, we could do a few things:

  1. Work on mimicking this behavior, continuing down our path of trying to import the client code inside of our server app, providing our own magic for dealing with SVG files.
  2. Change the build process for the client-side app so that the server can use the built version of the JavaScript and benefit from webpack's magic.
  3. Something else entirely.

The advantage to option 1 is that it would be an interesting problem to solve. We could change our custom import stuff to track what SVG gets imported and then work out some way of ensuring that its reference in the React causes the URL to appear instead of the content (perhaps by using a property accessor). The disadvantage is that this feels like work we should not need to do; like we're fighting against the current. There are already tools doing this magic work; why do we need to reimplement that magic? Why can we not just reuse it as it is?

With option 2, it means starting to unpack all the things that the create-react-app tool set up for us, taking control of the webpack bundling configuration and all that good stuff, so that we can import the production builds of files. Even if we managed it, it would not work for development, since our render server is not requesting files from the webpack dev server. In addition, it seems like the problem is now dictating odd parts of our solution; why should the client build be optimized only for server use when it also has to work in the client, which may have different concerns? There has to be a better way and there is. However, before we discover that better way, our option 3 – something else, we should first consider the other problem; our render server is trying to render file requests as web pages. What we need is for our server to serve the files themselves instead.

📤 Serving Files

To be clear, in a real world situation, we may well use a CDN to orchestrate some of this work. Based on its rules, it would either server files and cached renders, or request a new render from our render server, and our render server would obtain any files it needs either from the CDN or from a local folder that replicates those files. However, we want to learn more and so let's assume for now that we don't have a CDN and our render server is going to be the server browsers talk to first. Therefore, it's going to need to know how to serve the different types of requests; files, pages, etc.

This is a solved problem, so it should be pretty easy to get working. Part of the issue is that our handler currently assumes everything is a request for HTML. Instead, it should probably be looking at the Accept header in the request to determine what is needed and then act accordingly. However, we must consider both our final production behavior where files will be served from a static folder, and our development behavior where we are going to want to get those files from webpack's dev server so that we can benefit from its development time bundling and static file generation.

Production

Since development is going to be a little harder, let's work on production first, since that is not just easier, it's a lot easier. Thanks to the built-in static middleware that Express provides, we can specify our build folder as the source of static files and express does the rest.

if (process.env.NODE_ENV === "production") {
    app.use(express.static("../client/build"));
}

app.get("/*", (req, res) => res.send(
    renderPage(<App ssrLocation={req.url} />),
));

Before our route handler, we add a check for production, and if it passes, we add the middleware for the /static route. If we refresh our production server, amazingly, everything appears to work2!

Development

If only development was as easy. I mean it could be, we could just defer all our calls to the client webpack dev server and we would have a working site, but we would not be testing out any actual server-side rendering. Instead, we need to proxy everything to the webpack dev server first, including the web pages. Why the web pages too? Well, if you may recall earlier in this post, our early attempts had two server-side errors where we failed to interpret URLs. This was because the index.html template for development requires some string substitution that webpack handles for us. If our render server loads that template via webpack, not only will it give us the right substitutions, but it is going to embed the CSS and scripts for us too. Of course, since we also want to stick in some rendered HTML, we will need intercept those specific requests and add our own bits.

Before we can do any of that, we need to know the address of the client server. As it happens, it is currently the same as our render server; localhost:3000. That's a bit of a problem, so let's move our render server to 3001 while we're at it. That way, both the client development server and our render server can coexist.

There are quite a few changes I made to get this working, all of them in the render server itself. Since the index.html template for the development mode is served via the webpack dev server, I modified the template loading code to only support the production build template. I also updated the server to use port 3001 and I implemented changes to the route handler so that during development, we proxied requests to the webpack dev server. Finally, I modified our custom file importers to just import as their own filenames. I made this change because as we found out, reading the file contents was not helping us and in fact, may have actually been hindering our ability to debug. By having the filenames instead, it should give a better experience for us when we're debugging.

require("./require-patches.js");
const http = require("http");
const express = require("express");
const React = require("react");
const {renderToString} = require("react-dom/server");
const getProductionPageTemplate = require("./get-production-page-template.js");
const App = require("../client/src/App.js").default;

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

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

if (process.env.NODE_ENV === "production") {
    app.use(express.static("../client/build"));
}

app.get("/*", (req, res, next) => {
    /**
     * Production is easy. We'll load the template and render into it.
     */
    if (process.env.NODE_ENV === "production") {
        res.send(
            renderPage(getProductionPageTemplate(), <App ssrLocation={req.url} />),
        );
        next();
        return;
    }

    /**
     * During development, we're going to proxy to the webpack dev server.
     */
    if (process.env.NODE_ENV !== "production") {
        /**
         * Let's make life easier on ourselves and only accept UTF-8 when it's
         * a text/html request. We're in dev, so we don't need GZIP savings.
         */
        const headers = Object.assign({}, req.headers);
        if (req.headers["accept"] && req.headers["accept"].indexOf("text/html") > -1) {
            headers["accept-encoding"] = "utf8";
        }

        /**
         * Now call the client dev server, which we know is on port 3000.
         */
        http.get(
            {
                port: 3000,
                path: req.url,
                headers: headers,
            },
            (proxiedResponse) => {
                /**
                 * If our original request was text/html, we want to render
                 * react in there. So, let's gather that response and then
                 * render the page.
                 */
                if (req.headers["accept"] && req.headers["accept"].indexOf("text/html") > -1) {
                    let responseBody = "";
                    proxiedResponse.setEncoding("utf8");
                    proxiedResponse.on("data", (chunk) => {
                        responseBody += chunk;
                    }).on("end", () => {
                        res.send(
                            renderPage(responseBody, <App ssrLocation={req.url} />),
                        );
                        next();
                    }).on("error", e => {
                        res.status(500);
                        res.send(e);
                    });
                    return;
                }

                res.writeHead(proxiedResponse.statusCode, proxiedResponse.headers);
                proxiedResponse.pipe(res, {end: true});
                next();
            },
        );
    }
});

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

let productionTemplate;
export const getProductionPageTemplate = () => {
    if (!productionTemplate) {
        productionTemplate = fs.readFileSync("../client/build/index.html").toString();
    }
    return productionTemplate;
};
const fs = require('fs');

const requireText = function (module, filename) {
    console.log(`Pretend import of ${filename}`)
    module.exports = filename;
};

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

After making these changes, if we fully reload our development server, it appears to work! Fantastic! We're done right? Well, not quite. Though it may not be obvious, especially when compared with some of our earlier errors, we have some problems remaining:

  1. The HTML we render on the server has the wrong SVG path in it. Because we're rendering the non-production version of the App component in the server, it has no access to the magic sauce that webpack introduces. This is going to be the case for any other similar imports we do.
  2. We are not yet performing what React refers to as hydration. This means that all the work we did on the server is pretty much ignored once it reaches the client, and the React app is remounted from scratch. This may not seem important for our little app since it is already so fast (mainly because it does almost nothing), but for a bigger application all we have done is make serving the page slower with no actual benefit!

👋🏻 Until next time

Next time we will work on addressing these issues and finally have server-side rendering that works. Of course, even then our journey is not over. With a fundamental render server working, it will be prime territory for us to branch out into dealing with some gnarlier problems like server-side rendering parts of an app that rely on asynchronous operations like data requests, speeding up our app by only loading the parts we need, and how to optionally render components between server and client without getting rehydration errors.

As always, thanks for joining me on this journey through the lands of server-side rendering with React. Until next time, have a lovely week! 💙

  1. Sadly, we our site won't benefit from it whatsoever []
  2. It really is not, but we'll get to that another time. []

🖍 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. []

🏗 Creating An Express Server

Photo by Kelly Sikkema on Unsplash

This is part 4 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. [You are here] 🏗 Creating An Express Server
  5. 🖥 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 previous three posts in this series we have described what server-side rendering (SSR) is, created a simple application using React, and discussed the architecture of a privacy-aware server to ensure we understand some of the sharp edges around SSR. Now, we will actually implement a basic server. Just as with the React application we created, the server we create will not be a complete solution, but it will provide a foundation from which we can continue to explore SSR.

✨ A New Project

Where do we start? Well, we need a server that can receive web requests and respond to them. For that, I am going to use Express1, but first I need a project.

NOTE: Where you see yarn, know that you can use your own package manager as you see fit.

  1. Add a new repository on GitHub (or your source control platform of choice).
  2. Make a new folder locally for your code
  3. cd to your new folder and run git init
  4. git remote add origin <your github repo URL>
  5. git pull origin master
  6. git branch --set-upstream-to=origin/master
  7. Create and commit a .gitignore file
  8. Initialize it for JavaScript package management with yarn init
  9. Run yarn install to generate the lock file
  10. Commit the yarn.lock and package.json to the git repository

Great, so now we have a project we can start working on. Let's add Express.

yarn add express

This should update our package.json and yarn.lock, so don't forget to commit those changes. I also recommend pushing often to your remote repository, that way your code is backed up online in case your computer suffers a nasty accident2.

👋🏻 Hello World!

At this point we need to write some code. We need to setup a route for our server that can handle providing a rendered result for any URL that our application might have. There are a couple of ways we could do this:

  1. Assuming that our server is invoked by some intermediate layer, such as a cache, we could have the server implement a single route (e.g. /render) and pass the URL to be rendered as a query parameter.
  2. Our server could assume the URL is to be rendered by the client code and just accept any URL.

Option 1 gives us a great deal of flexibility in what our server can do, but it forces us to ensure that there is a layer between the original browser request and our server, as something has to be responsible for constructing the appropriate /render route request. Option 2 removes the need to have an intermediate layer, but it perhaps restricts us from expanding server functionality. Of course, option 2 can be changed to option 1 if the need arises, so we can go with option 2 for now, knowing that later, it can be updated to suit changing needs.

Normally, I would add lots of other things to this server to improve development and runtime investigations, such as linters, testing, and logging, but for the sake of brevity, right now we will stick to the main functionality.

const express = require("express");

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

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

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

This is our index.js file. It is not doing a lot. On line 4, we create our express app. On line 6, we tell it that for any route matching /*, return Hello World!. On line 8, we tell it to listen for requests on port 30003.

If we run this app with node index.js, we can go to our browser, visit any route starting with localhost:3000 and see the text, Hello World!. This is fantastic. We have a server and it is responding as we hope. Since we are going to run this often as we make changes, I will add a script to our package.json to run node index.js for us.

{
  "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"
  },
  "scripts": {
    "start": "node index.js"
  }
}

In the package.json file shown above, I have highlighted the section I added containing the new start command. From now on, we can start our app with yarn start. The next step is getting our server to render our React application. Before we do that, consider these questions:

  1. How does the server know about and load the code for our React application?
  2. How does the server get the rendered result to send back?
  3. How do we isolate render requests to avoid side-effects bleeding across requests?

🤔 The Hows

The answers to the first two questions have implications beyond the server itself, possibly influencing both our client application and any deployment process.

How our server knows about and loads our client application may affect how our server is deployed. Some server-side rendering solutions involve deploying the client-side code with the server so that it has direct access to the appropriate code, others use a mechanism such as looking up in a manifest to identify the files to load from a separate location (such as a content delivery network (CDN)). Neither of these is necessarily a bad choice – they both have their advantages and disadvantages. For example, deploying the server with the right code means:

  • ✅The server has fast access to the client application it is rendering
  • ✅The server can integrate nicely with the client application
  • ❌The render server must be deployed every time the client application changes
  • ❌The server is closely coupled to the client application

Whereas, looking up files in a manifest and loading them from elsewhere means:

  • ✅The render server rarely requires updating
  • ✅The server can render more than one application
  • ❌The server will probably need to cache JavaScript files locally or be at the mercy of latency when communicating with the CDN
  • ❌The client applications that the server renders likely need to include custom code to support being rendered by that server

Being aware of these approaches differ – and they differ in more than just the ways I have suggested, is useful in understanding the trade-offs we must make when implementing our render server. Perhaps answering the second question will help us decided which route to take; consider how will our server get a rendered result of the client application?

Our server is going to invoke a call from the React framework that renders our React application to a string, rather than mounting it inside the DOM of a browser. To do that, it needs a React component to render, so it must load our client application and get the root-level component. In addition, assuming our render server is rendering the entire page and not just the React component, the server is likely going to need to gather additional information, such as which files must be loaded in the page, the page title, etc.

This whole process of capturing the application render and associated metadata requires interplay between the server code and the client code. Revisiting the first question and the two approaches I gave: if the server has the client code deployed with it, the server could know exactly which files to load to render the component, importing those directly and using them accordingly; if the server is less-closely coupled, we likely need some mechanism whereby the client application itself does more of the heavy lifting by hooking into some framework provided by the server, even if that is just exporting a specific object so that server can identify the appropriate things to coordinate rendering.

Ultimately, either we have a server that is custom built to our application, or we have a server that is built to support many applications. What to do? I say, dive in and try them both. To that end, next time we will look at the first option where the server knows all about the client application (though we may cut some corners to get to the salient points), and we will answer that third question; how do we isolate our renders?

🙇🏻‍♂️ In Conclusion

Herein we have created our server, though it does not do much yet. We have also considered two different approaches to connect our server to our client application: closely-coupled or more open, and we have started to think about how the server will isolate and respond to render requests.

This week's entry turned out a little longer than I had intended, and covered less things than I had hoped. Sometimes that is the way it goes. One of the biggest reasons I write these blogs is to discover what I do and do not know about something. Often in the effort of explaining it to someone else, I identify a bias that I have without any supporting evidence, or a topic I grasp that is far harder to explain than I expected.

Until next time, when we start to implement our server-side rendering, please leave a comment. Perhaps you have a question, a personal experience writing a render server, or want to take umbrage at something I have stated. I look forward to learning with you as we continue this journey into the land of SSR. 🗺

  1. I find Express easy enough to use and well-supported, though there are other options that one could use instead if one were so inclined []
  2. A lesson from bitter experience; hard drives die (especially SSDs) without warning, drinks spill, laptops get dropped – keep your work backed up []
  3. The port is currently hard-coded for simplicity, but we could make this configurable []

🤷🏻‍♂️ What is server-side rendering (SSR)?

Featured image modified from photo by Andre Mouton on Unsplash

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

  1. [You are here] 🤷🏻‍♂️ 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. 🖍 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

One of my main responsibilities at work involves server-side rendering (SSR). From managing the services that perform SSR to the client components that developers use to build SSR-able frontends, I have my focus on many pieces of our frontend infrastructure. In this series of posts, I want to share some of the things I have learned and perhaps demystify this mostly fantastic approach to creating performant, stable, web experiences.

When the Internet started coming alive the first time, a lot of the magic was implemented on servers (aka server-side or "on the backend") that built HTML pages to deliver to web browsers. During this period, impressive collections of user interface components were created to make developing these server-based web apps easier and more reliable. Sadly, sites often felt a clunky and slow because the browser just was not equipped to do much beyond rendering the HTML it was given; JavaScript execution was too slow for anything very meaningful. Even button clicks in the browser would cause a new request to the server that would then generate a whole new page for the browser.

Then Google Chrome and the v8 JavaScript engine came along and changed everything. Browsers became blessed with speedy JavaScript engines. That meant we could do a lot of this work in the browser (aka client-side or "on the frontend") and develop applications that could properly divide presentation (the application running in the browser) from data (the database and CRUD1 operations running in the backend). From this new power came the concept of the single page app (SPA), where one page comes from the server and then does most of its work client-side, deferring to the backend only when data is read or written. Often, the page is received from the server in an initial state and then subsequent data requests may populate that page (imagine your Facebook feed loading) to get it ready for you to use. However, this can mean that the time to interactive – the length of time before a user can actually use the page – is long. This affects all sorts of things, but particularly user retention. Folks don't like waiting and if they wait too long, they become frustrated and eventually bounce2.

Much like in the backend era prior to faster frontend JavaScript execution, new frameworks and user interface components have appeared that help to create powerful web apps using browser-based JavaScript. Things like Angular, Ember, and React (there are more – there are always more3). However, there can still be a mismatch between backend and frontend. To get a nice experience for our users, code runs on the backend to build an initial page and then that is handed to the frontend, which promptly takes over. Sometimes, this transition is nice and smooth, but other times it is not. More importantly, there are at least two different code paths for generating the page; at least one backend one and at least one frontend one4.

Having more than one code path trying to do equivalent work is hard to maintain. A change in one place may or may not need a change in the other, and either way, careful quality engineering is needed to make sure bugs are not introduced. The shift away from web apps executing entirely on the backend but rendered on the frontend to being executed mostly on the frontend with a bit of backend increased the complexity of the code for anyone that wanted a performant, engaging web site. You had a choice; either keep the separation of frontend for presentation and backend for data, and have a slower initial website experience, or blur the line and have more complex code, but a nicer user experience. Thankfully, folks thought about this and like those responsible for React, came up with a solution – server-side rendering (SSR).

Thanks to the JavaScript Revolution that started with v8, we now are able to run JavaScript outside of our web browsers (using NodeJS, for example). This creates some interesting opportunities for running the same code in both the frontend and backend. This does not mean that all the code would run in two places – we may want to keep the CRUD operations as a backend thing; however, being able to run our presentation code in both places means we can overcome some of that delay when a user first visits a page of our site. We can use the same JavaScript that would render our page in the web browser to render a version of our page on the server and then let the browser take over, all with a single codebase5.

Server-side rendering (SSR) – The rendering of a web page on a server rather than in a browser

And in the context of what I want to write about, that is server-side rendering (more commonly referred to as SSR, at least by me, anyway) – the rendering of a web page on a server rather than in a browser. In fact, it's so similar to rendering in a web browser, I have started to refer to the server responsible for SSR as a server-side browser. This tends to reframe how folks think of problems they face and how to start thinking about frontend code not as "does this run in the server or the client?" but "what browsers does this have to support?". It turns out that second question is much more familiar to most frontend developers than the first.

For now, I will leave things there. I think this post is quite long enough. Thank you for reading. Over the next few posts, we will look at creating an app using React that supports SSR, as well as a backend browser to perform that SSR, and the implications that SSR has when it comes to writing frontend code.

  1. Create Read Update Delete []
  2. there's a reason it's called "bounce rate" []
  3. While I was writing this, I expect three more frontend frameworks came into being and at least one died []
  4. There are cases where different parts of the same page are rendered by different services; front or backend – talk about complicated []
  5. Not only that, but the server response could be cached with a CDN (content delivery network) to make our sites even faster! []

🙇🏻‍♂️ Introducing checksync

Photo by Clint Adair on Unsplash

Have you ever written code in more than one place that needs to stay in sync? Perhaps there is a tool in your framework of choice that can generate multiple files from a single source of truth, like T4 templates in the .NET world; perhaps not. Even if there is such a tool, it adds a layer of complexity that is not necessarily easy to grok. If you look at the output files or the template itself, it may not be clear what files are affected or related.

At Khan Academy, we have a linter, written in Python, that is executed whenever we create a new diff for review. It runs across a subset of our files and looks for blocks of text that are marked up with a custom comment format that identifies those blocks as being synchronized with other target blocks. Included in that markup is a checksum of the target block content such that if the target changes, we will get an error from the linter. This is our signal to check if further changes are need and then update the checksums that are invalidated. The only bugbear folks seem to have is that instead of offering an option to auto-fix checksums in need of update, it outputs a perl script that has to be copied and run for that purpose.

Small bugbear aside, this tool is fantastic. It enables us to link code blocks that need to be synchronized and catches when we change them with reasonably low overhead. Though I believe it is hugely useful, it is sadly custom to our codebase. I have long wanted to address that and create an open source version for everyone to use. checksync is that open source version.

🤔 The Requirements

Before writing checksync, I started out with the following requirements:

  • It should work with existing marked up code in the Khan Academy codebase; specifically,
    1. File paths are relative to the project root directory
    2. Checksums are calculated using Adler-32
    3. Both // and # style comments are used to comment the markup tags
    4. Start tag format is:
      sync-start:<ID> <CHECKSUM> <TARGET_FILE_PATH>
    5. End tag format is:
      sync-end:<ID>
    6. Multiple start tags can exist for the same tag ID but with different target files
    7. Sync tags are not included in the checksum'd content
    8. An extra line of blank content is included in the checksum'd content (due to a holdover from an earlier implementation)
    9. .gitignore files should be ignored
    10. Additional files can be ignored
  • It should be comparably performant to the existing linter
    • The linter ran over the entire Khan Academy website codebase in less than 15 seconds
  • It should auto-update invalid checksums if asked to do so
  • It should output file paths such that editors like Visual Studio Code can open them on the correct line
  • It should support more comment styles
  • It should generally support any text file
  • It should run on Node 8 and above
    • Some of our projects are still using Node 8 and I wanted to support those uses

With these requirements in mind, I implemented checksync (and ancesdir, which I ended up needing to ensure project root-relative file paths). By making it compatible with the existing Khan Academy linter, I could leverage the existing Khan Academy codebase to help measure performance and verify that things worked correctly. After a few changes to address various bugs and performance issues, it is still mildly slower than the Python equivalent, but the added features it provides more than make up for that (especially the fact that it is available to folks outside of our organization).

🎉 Check It Out

checksync includes a --help option to get information on usage. I have included the output below to give an overview of usage and the options available to customize how checksync runs.

checksync --help
checksync ✅ 🔗

Checksync uses tags in your files to identify blocks that need to remain
synchronised. It works on any text file as long as it can find the tags.

Tag Format

Each tagged block is identified by one or more sync-start tags and a single
sync-end tag.

The sync-start tags take the form:

    <comment> sync-start:<marker_id> <?checksum> <target_file>

The sync-end tags take the form:

    <comment> sync-end:<marker_id>

Each marker_idcan have multiple sync-start tags, each with a different
target file, but there must be only one corresponding sync-endtag.

Where:

    <comment>       is one of the comment tokens provided by the --comment
                    argument

    <marker_id>     is the unique identifier for this marker

    <checksum>      is the expected checksum of the corresponding block in
                    the target file

    <target_file>   is the path from your package root to the target file
                    with a corresponding sync block with the same marker_id

Usage

checksync <arguments> <include_globs>

Where:

    <arguments>       are the arguments you provide (see below)

    <include_globs>   are glob patterns for identifying files to check

Arguments

    --comments,-c      A string containing comma-separated tokens that
                       indicate the start of lines where tags appear.
                       Defaults to "//,#".

    --dry-run,-n       Ignored unless supplied with --update-tags.

    --help,-h          Outputs this help text.

    --ignore,-i        A string containing comma-separated globs that identify
                       files that should not be checked.

    --ignore-files     A comma-separated list of .gitignore-like files that
                       provide path patterns to be ignored. These will be
                       combined with the --ignore globs.
                       Ignored if --no-ignore-file is present.
                       Defaults to .gitignore.

    --no-ignore-file   When true, does not use any ignore file. This is
                       useful when the default value for --ignore-file is not
                       wanted.

    --root-marker,-m   By default, the root directory (used to generate
                       interpret and generate target paths for sync-start
                       tags) for your project is determined by the nearest
                       ancestor directory to the processed files that
                       contains a package.json file. If you want to
                       use a different file or directory to identify your
                       root directory, specify that using this argument.
                       For example, --root-marker .gitignore would mean
                       the first ancestor directory containing a
                       .gitignore file.

    --update-tags,-u   Updates tags with incorrect target checksums. This
                       modifies files in place; run with --dry-run to see what
                       files will change without modifying them.

    --verbose          More details will be added to the output when this
                       option is provided. This is useful when determining if
                       provided glob patterns are applying as expected, for
                       example.

And here is a simple example (taken from the checksync code repository) of running checksync against a directory with two files, using the defaults. The two files are given below to show how they are marked up for use with checksync. In this example, the checksums do not match the tagged content (though you are not expected to know that just by looking at the files – that's what checksync is for).

// This is a a javascript (or similar language) file

// sync-start:update_me 45678 __examples__/checksums_need_updating/b.py
const someCode = "does a thing";
console.log(someCode);
// sync-end:update_me
# Test file in Python style

# sync-start:update_me 4567 __examples__/checksums_need_updating/a.js
code = 1
# sync-end:update_me
Example output showing mismatched checksums

Additional examples that demonstrate various synchronization conditions and error cases can be found in the checksync code repository. To give checksync a try for yourself:

I hope you find this tool useful, and if you do or you have any questions, please do comment on this blog.