Logo / QR code

How I’ve Built My First React Application IV

Part 4: Isomorphism & server-side rendering

This is the fourth and final article of my How I’ve Built My First React Application series, showing the steps I’ve taken trying to build an isomorphic voting application using React. All the code is available at my GitHub repo: question-it.


In this article, I will talk about Isomorphism: What is an isomorphic application, and how you could make one with React, GraphQL, and Relay.

Intro

An isomorphic application is an application that can run both on the client, and the server. With the arrival and increasing popularity of Node.js, JavaScript has become a reliable server-side language. This, along with the dominance over client web applications, made developing isomorphic JavaScript applications doable.

Isomorphic web app structure
Isomorphic Javascript: The Future of Web Apps

Why?

Pure server side rendering
ASP.NET is one example of pure server-side rendering

Traditionally, web applications became available through almost pure server-side rendering. The client would request a page, and the server will use the data he has to render the HTML that would be sent to the client. This could have been done using ASP.NET, Java’s JSP, or any other server-side technology. The downside of that is that, with a lot of users, the application would require a very strong infrastructure to handle the load, even if the application itself wasn’t complex.

Single Page Applications

Later began the era of client-side rendering. With front-end frameworks like AngularJS and Ember, creating SPA (Single Page Applications) was easier than ever. The client would ask for a resource, and the server would simply send a basic HTML skeleton, with a bunch of JavaScript files that make everything work, and in the case of SPA would even handle the routing inside the app.

The server had a lot less to do, and the developer could spare some resources and money. But client-side applications weren’t perfect, too. As the client was required to download almost all of the application’s code, the initial rendering of the app toke its time, and the user experience degraded. This was more noticeable, as the application became larger.

React & Ember

And then came isomorphic applications. With applications that could be run both on the server and client, JavaScript applications could enjoy the benefits of both approaches and only a bit of the downside. When the client would ask for a resource, our rendering library will run on the server and generate the HTML to be sent to the client, along with all the application code. The client wouldn’t need to wait as he would do with a pure client-side application. And what about the server? Well, after the server does the initial rendering, most of the rest of the work would be done on the client, the same as it is being done with Angular applications.

Isomorphic react applications, or isomorphic applications in general, could be organized into three folders:

  • client: holding all the client-side-specific code. Mounting the root React component on some DOM elements.
  • server: holding all the server-side-specific code. Handling requests and serving HTML.
  • shared: holding the code that will be used both on the client and the server. React components and other application code can be found here.

So how to make isomorphic applications with React?

Isomorphic Building

Having code that runs both on the client and the server, means that we have to build the code for both environments: the browser, and Node.js. That’s because web applications built with webpack wouldn’t run on Node.js.

Luckily enough, webpack can be configured to build the code for Node.js, using the configuration option:

{
  target: ‘node’, // defaults to ‘web’
  ...
}

This means you would have to create another webpack configuration for the server. This can be somewhat tedious, as it makes repetitive code, resulting in changes to be made on both files. Plus, it can be pretty complex for beginners.

Fortunately, thanks to GitHub user halt-hammerzeit, building isomorphic applications can be simple again. He created a tool named universal-webpack which converts client webpack configuration to server webpack configuration.

All the needed documentation can be found in the repo.

Isomorphic React

To make the isomorphism work with React, usually, only server-side code should be messed with. On the client code, the code should be pretty much the same:

import ReactDOM from 'react-dom’;
import App from '../shared/components/root’;

ReactDOM.render(<App />, document.getElementByID('react-root'));

On the server, we can’t mount components, instead, we render our component into a string, which then will be sent to the client as HTML. The react-dom library has a specific function for that: renderToString.

The relevant code will look like this (Please note that this code doesn’t use a routing library that matches component to URL):

import { renderToString } from 'react-dom/server’;
import ComponentToRender from '../shared/components/componentToRender’;

/*
 * INSERT MORE CODE HERE
 */

app.get('some url', (req, res, next) => {
  // this url matches the component that should be renderd
  const componentHTML = renderToString(InitialComponent);
  const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div>
          /* This div will later be used to mount the entire application using the client code */
          /* insert script tags here */
        </body>
      </html>
    `;
  res.end(HTML);
});

Isomorphic Routing

Many applications nowadays are Single Page Applications: applications that are essentially a single page sent from the server to the client. In those applications almost all route handling is done on the client, using some routing library. In our case, the most popular routing library for React is React Router.

I’m not going to teach you how to use React Router here, only how it can be used for isomorphic applications. If anything’s not clear, you should probably go ahead to React Router’s GitHub repository, where you can find very detailed documentation.

Usually, the client code remains untouched, but in case async routes are being used you need to assure the async behavior has been resolved before rendering. This can be done using the match function:

import ReactDOM from "react-dom";
import { Router, browserHistory, match } from "react-router";

import routes from "../shared/routes";

match({ history, routes }, (error, redirectLocation, renderProps) => {
  render(<Router {...renderProps} />, document.getElementById("react-root"));
});

The server code looks the same whether you’re using async routes, or not. On the server we’re sending the request URL to match, which calls a callback with all the information required for the initial render:

import { renderToString } from 'react-dom/server’;
import routes from '../shared/routes';

// react-rounder will handle the routing
app.get('*', (req, res, next) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    // react-router encountered an error
    if (error) {
      res.status(500).send(error.message);
    }
    // redirect from Redirect or IndexRedirect
    else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    }
    // found a route, RouterContext will render the needed components according to renderProps
    else if (renderProps) {
     const componentHTML = renderToString(<RouterContext {...renderProps} />);
      const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div>
          /* This div will later be used to mount the entire application using the client code */
          /* insert script tags here */
        </body>
      </html>
    `;
     res.status(200).send(HTML);
    }
    // didn't match any route
    else {
      res.status(404).send('Not found');
    }
  });
});

And now you got Isomorphic Routing!

Isomorphic Relay

If you’re using Relay (as I did), for data fetching in your React application. You may have wondered, whether you could make the data fetching isomorphic. That means when the server will render a React component, it will also resolve and pass the initial data the component requires. That way, the client wouldn’t need to send another request for the data, after the initial rendering.

At the time of writing, Relay has no server-side rendering support. Fortunately, as with many other open source projects, you can count on someone to take care of that:

The package isomorphic-relay by denvned does that exactly. Isomorphic Relay accomplishes that by using another Relay Network Layer on the server, that saves the resolved data requests, and sends that data to the client, along with the rendered component. Then, the client injects that data into its’ Relay store, and because Relay is smart, it won’t request that data again.

For React Router users that integrate React Router with Relay using react-router-relay, there’s also isomorphic-relay-router, also created by denvned. Isomorphic Relay Router adds server-side rendering support to react-router-relay using isomorphic-relay. Similar to the plain Isomorphic Relay package, Isomorphic Relay Router requires a very simple setup:

app.get("*", (req, res, next) => {
  match(
    { routes, location: req.url },
    (error, redirectLocation, renderProps) => {
      if (error) {
        res.status(500).send(error.message);
      } else if (redirectLocation) {
        res.redirect(302, redirectLocation.pathname + redirectLocation.search);
      } else if (renderProps) {
        IsomorphicRouter.prepareData(renderProps, networkLayer)
          .then(render)
          .catch(next);
      } else {
        res.status(404).send("Not found");
      }

      function render({ data, props }) {
        const componentHTML = renderToString(IsomorphicRouter.render(props));
        const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div> 
          // passing the data to the client
          <script id="preloadedData">
            ${JSON.stringify(data)}
          </script>     
          /* insert script tags here */
        </body>
      </html>
      `;
        res.status(200).send(HTML);
      }
    }
  );
});

And once the client has the data, it just needs to inject it into the relay store, and render:

import IsomorphicRouter from "isomorphic-relay-router";

const environment = new Relay.Environment();

environment.injectNetworkLayer(new Relay.DefaultNetworkLayer("/graphql"));

const data = JSON.parse(document.getElementById("preloadedData").textContent);

IsomorphicRelay.injectPreparedData(environment, data);

const rootElement = document.getElementById("react-root");

match(
  { routes, history: browserHistory },
  (error, redirectLocation, renderProps) => {
    IsomorphicRouter.prepareInitialRender(environment, renderProps).then(
      (props) => {
        ReactDOM.render(<Router {...props} />, rootElement);
      }
    );
  }
);

Isomorphic Styling

Isomorphic styling? What’s that? How could the server render CSS, and even if it can, what would that even mean?

Isomorphic style loading is pretty much a made-up term that refers to rendering critical-path CSS during server-side rendering. Critical path CSS is the part of the application CSS files, that is critical for what is rendered for the user. Using the normal webpack style-loader, all imported css files are injected into the HTML as style tags. On the other hand, using isomorphic-style-loader by kriasoft will inject only critical CSS, so unmounted components’ CSS will not be injected. This optimizes the Critical Path Rendering and allows for a better user experience and a shorter loading time.

To make isomorphic-style-loader work, on the server you need to pass to the components an insertCss function as a React context:

app.get("*", (req, res, next) => {
  match(
    { routes, location: req.url },
    (error, redirectLocation, renderProps) => {
      if (error) {
        res.status(500).send(error.message);
      } else if (redirectLocation) {
        res.redirect(302, redirectLocation.pathname + redirectLocation.search);
      } else if (renderProps) {
        IsomorphicRouter.prepareData(renderProps, networkLayer)
          .then(render)
          .catch(next);
      } else {
        res.status(404).send("Not found");
      }

      function render({ data, props }) {
        const css = [];
        const InitialComponent = (
          <Root onInsertCss={(styles) => css.push(styles._getCss())}>
            {IsomorphicRouter.render(props)}
          </Root>
        );
        const componentHTML = renderToString(InitialComponent);
        const HTML = `
      <!DOCTYPE html>
      <html>
        <head>
          /* insert meta, link, style and script tags here */
          <script type="application/javascript" src="bundle.js"/>
          <style type="text/css">${css.join("")}</style>
        </head>
        <body>
          <div id="react-root">${componentHTML}</div> 
          // passing the data to the client
          <script id="preloadedData">
            ${JSON.stringify(data)}
          </script>     
          /* insert script tags here */
        </body>
      </html>
      `;
        res.status(200).send(HTML);
      }
    }
  );
});

Here the insertCss function is passed as a prop to a dumb component Root, which will just pass this prop to the context.

Then, what is left to do is export a container for each component that will use insertCss:

import React from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './MyComponent.css';

class MyComponent extends React.Component {
  ...
}
export default withStyles(s)(MyComponent);

withStyles just creates a container for the component, and inserts the given css on component mount.

That is basically all there is to know about creating isomorphic applications with React, but in case I’ve missed something, I’d be happy to hear!

And that is the end of my How I’ve Built My First React Application series :)

Previous: How I’ve Built My First React Application III
Next: Kusto ❤️ FluentBit