How I’ve Built My First React Application: Isomorphism

Part 4: Isomorphism

This is the fourth and last article of my How I’ve Build 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

Isomorphic application is an application which can run both in 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.

Airbnb Isomorphic Javascript: The Future of Web Apps

Why?

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 for a page, and the server will use the data he has to render the html that would be sent for 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.

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 a resource, and the server would simply send a basic html skeleton, with a bunch of JavaScript files that makes everything works, 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 user experience has degraded. This was more noticeable, as the application became larger.

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 downsides. When the client would ask for a resource, our rendering library will run on the server and generate the html to be sent on the client, along with all the application code. The client wouldn’t need to wait as he would do with 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, same as it is being done with Angular applications.

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

  • client: holding all the client-side specific code. Mounting the root React component on some DOM element.
  • 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 which 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 a 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';
/*
* INSERT MORE CODE HERE
*/
// 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 it 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 others 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 integrates 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 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 to 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 to 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 a better user experience and less loading times.

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 exporting 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, be sure to check it: