Isomorphic React Relay

What is isomorphism?

In web development, the term isomorphic application applies to an application whose code can run both in the server and the client. In the case of React the code is naturally written in JavaScript. While React has good support for isomorphism in itself, making a Relay application isomorphic can be less than trivial.

The word "Isomorphic" comes from the Greek words "isos" for "equal" and "morph" for "shape". Isomorphism describes that if you look at the same entity in two different contexts, you should get the same thing. In the case of web application the contexts are server and client. Although the term has been mostly used in mathematics until now, it's an apt term to describe a web programing pattern where the code is shared by the front-end and back-end.

Isomorphism is often a cure for the disadvantages of Single Page Applications (SPAs). Single page applications perform all the rendering and composition of DOM elements on the client side. Their communication with the server includes packets of data rather than fragments of HTML to be displayed. The main advantages are quick response to user input, and reduced bandwidth requirements while operating the application.

There are disadvantages to SPAs too:

Isomorphism offers a solution to these two problems:

Routing

React router is the default solution for routing in the world of React. It has a flexible and powerful and owing in terms of design to the Ember router. When doing isomorphic applications, the routing needs to be performed both on the client and on the server. For instance, when the application has already loaded, and we are working as in a regular SPA, we might be on the page with list of essays and then click on an essay to navigate to the detail page. Here is the code that does that:

class Ensayo_PublicListing extends React.Component
{
  _handle_onClick( id )
  {
    this.context.router.push( '/Ensayo_PublicListing/' + id );
  };

  renderEnsayos( )
  {
    return
      ...
        <CardText
          onClick={ ( ) => this._handle_onClick( edge.node.id ) }
        >
      ...
    );
  }

  Ensayo_PublicListing.contextTypes = {
    router: React.PropTypes.object.isRequired,
  };
}

The URL for the details page has the format of:

http://<host>/Ensayo_PublicListing/<id of essay>

It is defined in the routes.js file. Here is the part of the definition relative to the essays:

import Chrome from './components/Chrome.jsx';
import Ensayo_PublicItem from './components/Ensayo_PublicItem.jsx';
import Ensayo_PublicListing from './components/Ensayo_PublicListing.jsx';
import Home_Screen from './components/Home_Screen.jsx';
.........

import ViewerQueries from './queries/ViewerQueries';

export default [
  {
    path: '/',
    component: Chrome,
    queries: ViewerQueries,
    indexRoute: {
      component: Home_Screen,
      queries: ViewerQueries,
    },
    childRoutes: [
      .........
      {
        path: 'Ensayo_PublicListing',
        indexRoute: {
          component: Ensayo_PublicListing,
          queries: ViewerQueries,
        },
        childRoutes: [
          {
            path: ':id',
            component: Ensayo_PublicItem,
            queries: ViewerQueries,
          },
        ],
      },
      .........
    ],
  },
];

When the application is already loaded and works as an SPA, the routes are passed to IsomorphicRouter.Router component, which initializes react router under the surface. Here is how it is done in app.js:


import { browserHistory } from 'react-router';

// First load isomorphic-relay:
import IsomorphicRelay from 'isomorphic-relay';
// And only then load react-relay:
import Relay from 'react-relay';

import IsomorphicRouter from 'isomorphic-relay-router';
import React from 'react';
import ReactDOM from 'react-dom';

.........
import routes from './routes';

.........

const rootElement = document.getElementById('root');

ReactDOM.render(
    <IsomorphicRouter.Router routes={routes} history={browserHistory} />,
    rootElement
);

The isomorphic router can read the format of the routes.

On the other hand, a user can load the essay page in the browser by URL, for instance following a shared link, link in an email, etc. In this case server rendering of the page will be performed. In order to determine the right page to start with, express routing is used. First, the server for the web app server.js simply acrs as a wrapper around renderOnServer.js:

let app = express();

// Serve HTML
app.get( '/*', ( req, res, next ) => {
    renderOnServer( req, res, next, assetsPath );
} );

The real work is done in renderOnServer.js:

import IsomorphicRouter from 'isomorphic-relay-router';
import path from 'path';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Relay from 'react-relay';
import {match} from 'react-router';
.........

import routes from './routes';
.........

export default ( req, res, next, assetsPath ) =>
{
  .........

  match( { routes, location: req.originalUrl }, ( error, redirectLocation, renderProps ) =>
    {
      queue.push(
        queueTask =>
        {
          .........

          if( error )
            next(error);
          else if( redirectLocation )
            res.redirect( 302, redirectLocation.pathname + redirectLocation.search );
          else if( renderProps )
            IsomorphicRouter.prepareData( renderProps ).then( render, next );
          else
              res.status( 404 ).send( 'Not Found' );

          function render( { data, props } )
          {
            .........
          }
        },
        .........
      );
    }
  );
};