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:
A typical web application is accessible to search engines. For instance a page with an Essay will have a specific URL which can be used to access it, and HTML structure including <title>, <meta>, <h1>, <h2> tags, with the text of the article separated into paragraphs in different <div> tags. In the case of a single page application, however, there is no page that can be made accessible to a search engine. All the engine could hope to see is a jiberish of (hopefully) minified unreadable JavaScript and JSON documents with unknown structure.
Another side effect of using an SPA is that in order for the user to be able to see any information in the browser, all the code needed to render this information needs to be loaded. Typically the size of that code is significantly larger than the generated HTML.
Isomorphism offers a solution to these two problems:
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,
},
],
},
.........
],
},
];
path: ':id'
declaration indicates that the Id will be retrieved from the URL and passed to the component as a parameter.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 } )
{
.........
}
},
.........
);
}
);
};