⚡️ 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 if 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 A React App

Photo by Rolands Zilvinskis on Unsplash

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

  1. 🤷🏻‍♂️ What is server-side rendering (SSR)?
  2. [You are here] ✨ 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

Last week, I gave my own brief history of web sites and how their frontend implementation has drifted from entirely server-based, to entirely client-based, and is now settling (perhaps) with hybrid that we call server-side rendering (SSR). The goal of this journey is to poke around the gnarly bones of SSR and learn what we learn. We may make mistakes, break idioms, and portray ourselves as fools, but we will definitely learn. For that reason, we are not going to bother with things like Next.js, which have already fleshed over and hidden away the gnarly bones for us1.

Now, before we dip even further in the specific world of SSR, we are going to need an app. To be specific, we are going to need a React app.

Hello, React World!

Before creating a React app, we need a package manager; either npm or yarn will suffice. Though we could put together our own app from scratch, there is no need to as the handy create-react-app package exists. There are numerous ways to use this, but the easiest is to use yarn create or npx, which will do the work of obtaining the package and executing it all in one go.

For yarn, drop create from the front of create-react-app:

yarn create react-app <app-name-here>

For npx:

npx create-react-app <app-name-here>

I want to also put this in a git repository so I can track my changes. I would normally make a directory, run git init and then get started. In this case, we do not need to as create-react-app takes care of that for us.

So, let's begin. Open a terminal and invoke create-react-app.

yarn create react-app hello-react-world

After executing this, you will have a working React app that uses react-scripts to manage the basics. This is perfect for our initial journey. If we navigate to that directory and run the project, we can see our app in action.

cd hello-react-world
yarn start
Screenshot of the website created by the create-react-app tool. It shows the React logo in blue, over a dark grey background, with the text "Edit src/App.js and save to reload." and a link labelled, "Learn React"

Fantastic. We have an app. Before we do any more, let's get some remote source control underway. I am paranoid of my machine dying and losing all my work, so having an off machine place to store things is really useful. First, add a new repository on your source control site of choice (I prefer GitHub). Second, connect the local repository to the remote one:

git remote add origin <repo-url>
git fetch
git branch --set-upstream-to origin/master master

Since we want our local code to be the first commit and we're pushing to a brand new repository, we can force push what we have.

git push -f

Routing

Okay, we have an app and it is in source control. This is usually a good spot to spend some time setting up code quality tools like eslint and prettier. I am going to be naughty and skip right over that right now and save it for a different post, perhaps. Instead, let us add some routing to our fledgling application.

There are a few options for implementing routing in a React application (some frameworks, like Next.js, even provide it out of the box). We are going to use React Router. There are two variants of React Router; one for React on websites, and one for React Native on mobile. We want the website variant, which is provided by the react-router-dom package.

yarn add react-router-dom

Now we will edit our app to have a couple of routes. The main app is defined in the src/App.js file. It should look something like this. I have highlighted the lines we're going to replace; we are also going to add some too.

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

There are two things we want to add.

  1. The routes to render our pages.
  2. The links to navigate to our routes.

First we import four things from react-router-dom:

  1. BrowserRouter
    This is the root of our React Router-based navigation. Basically, the router is responsible for the routing (I'm sure you guessed that).
  2. Link
    This replaces the anchor tag (<a>) for our navigation.
  3. Route
    This is used to render a matched route.
  4. Switch
    This allows us to specify a table of possible routes that can be used to work out what should handle the URL currently being viewed.

With these things, we can then add some routes. I am adding Home, About, and Contact. Here is my app code after the edit. I have highlighted the new lines.

import React from 'react';
import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <BrowserRouter>
      <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>
    </BrowserRouter>
  );
}

export default App;
Screenshot of the updated app using React router. The top shows the React logo with three links below it for "Home", "About" and "Contact" all on a dark grey background. Below that is a white region with the text "This is the home page!"

I also edited the CSS a little, but only to make things easier to see. The important bits are the router, wrapping our app, the Link components to perform navigation, and the Route components that render each route. With this, we now have a single page React app that has three pages for home, about, and contact.

This is going to be the application we will eventually render on the server. The important take away at this point is that we are not going to change the functionality of this app in order to achieve our aim. There are some changes we must make to support SSR, but we will not have two versions of the code. The code that runs in the browser will run on the server.

Next time, we are going to setup a server that will perform our SSR and consider what changes we need to make to our application infrastructure in order to support it. We might even get our first server-side rendered page. Until then, thanks for joining me on this continued exploration of server-side rendering using React.

  1. of course, if starting a new project knowing you need SSR, you should explore solutions like Next.js []