ponyfoo.com

Server-side React Layout & Side Effects

Fix

We’ve been hard at work last week analyzing how to build a universal app using React. First we looked at the bare minimum needed to run Babel through Browserify for ES6 and JSX support, as well as how to render a basic app seamlessly in the server and the browser. On friday we added react-router so that view routing is handled for us on both the server-side as well as the client-side – universally. Today we’ll be making some tweaks to what we have so far.

Particularly, we’ll be moving the server-side layout rendering out of express-hbs and into another React component, the <Layout /> component, which will only be rendered on the server. The reason for that change is one of consistency – instead of having to understand (and deal with) two different templating languages in Handlebars and JSX, we’ll only be dealing with JSX.

After making that change we’ll be looking at a couple of react-side-effect-powered libraries. One of these will enable us to set the document.title declaratively in our views from anywhere in our JSX templates. The other allows us to define <meta> tags anywhere in the document as well.

Let’s get on with it!

Modularizing the router

The first order of business will be turning our router into an individual module. So far it was just another method in app.js, but it’d be a bit neater to place it in a file of its own, reducing complexity and finally decoupling app.js from the knowledge of how views are rendered, or even routed.

You should add this line to app.js, after cutting the router method from that module.

import router from './router';

Now we can move the router to a module of its own, the only addition would be a export default prefix so that the module exports an API – that’s exactly the router method.

import React from 'react';
import Router from 'react-router';
import routes from './routes';

export default function router (req, res, next) {
  var context = {
    routes: routes, location: req.url
  };
  Router.create(context).run(function ran (Handler, state) {
    res.render('layout', {
      reactHtml: React.renderToString(<Handler />)
    });
  });
}

What was bugging me at this point was rendering the layout in Handlebars while every view component was using JSX.

Rendering the Layout with JSX

I decided to give a shot to a React component for the layout as well. That way I’d get more cohesiveness across the codebase (by sticking to a single template engine), as well as . This component would only be used on the server-side, and we would still render the contents of each individual view separately, as we did before. The distinction is useful for the client-side rendering part, as the layout itself won’t be changing – for the most part.

In the snippet below I introduced an import statement that’ll get us the <Layout /> component, and changed the rendering mechanism to render the layout via React instead of Handlebars. Note how I’m passing the main HTML for the partial to the layout.

import React from 'react';
import Router from 'react-router';
import Layout from './components/Layout';
import routes from './routes';

export default function router (req, res, next) {
  var context = {
    routes: routes, location: req.url
  };
  Router.create(context).run(ran);
  function ran (Handler, state) {
    var doctype = '<!doctype html>';
    var main = React.renderToString(<Handler />);
    var full = React.renderToString(<Layout main={main} />);
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write(doctype + full);
    res.end();
  }
};

JSX is Awkward

Keeping the <!doctype> out of the JSX template is kind of awkward, but I couldn’t figure out a way to include it in the JSX. The problem is that JSX has an XML style to its templates – meaning you have to use self-closing <meta /> and <link /> tags. This also makes it hard for you to render HTML comments <!-- foo --> – I haven’t figured out a way how to render those either! Of course, most of the time, you don’t want to declare a <!doctype> nor to add HTML comments (as opposed to comments in the template itself, which are easy to add).

An important portion of the equation is how the Layout file should look like. Here’s a first version of the layout based on what we used to do in Handlebars. It leverages the main partial, which you can access as this.props.main in the React component.

import React from 'react';

export default class Layout extends React.Component {
  render () {
    const { main } = this.props;

    return (
      <html>
        <body>
          <main dangerouslySetInnerHTML={{__html: main}} />
          <script src='/bundle.js'></script>
        </body>
      </html>
    );
  }
};

The dangerouslySetInnerHTML property has the noble intention to warn you against injecting unsanitized user input into your site, it’s documentation is very pompous in a “you’ll probably get it wrong, so we made it a very weird API” way. Imagine if everything you could possibly get wrong had a shitty API just so you double check. Wouldn’t it be better to just educate developers about XSS and user-input sanitization? </rant>

Setting the <title>

When it comes to universal rendering sometimes even the simplest things can become annoying to deal with. One of those things is setting the title of your web pages. Your engine of choice has to be able to set the title on the layout on the server-side directly and then be also able to change document.title dynamically on the client-side whenever the view changes. In the case of React I came across a small module that helps me achieve universal view titles with little effort.

First off, we’ll be installing react-document-title. This is a React component you can include in your JSX templates to set the title.

npm i react-document-title -S

A nice aspect of how it works is that you can add as many <DocumentTitle title='foo' /> tags in your component, and nest them as deep as you want. The react-side-effect library underlying react-document-title then figures out what tag was added last, and returns that when DocumentTitle.rewind() gets called.

Given that this works across all components, we are able to pull the title that was defined within one of the partial views, and then we can apply it to the document directly on the server-side.

import React from 'react';
import DocumentTitle from 'react-document-title';

export default class Layout extends React.Component {
  render () {
    const { main } = this.props;

    return (
      <html>
        <head>
          <title>{DocumentTitle.rewind()}</title>
        </head>
        <body>
          <main dangerouslySetInnerHTML={{__html: main}} />
          <script src='/bundle.js'></script>
        </body>
      </html>
    );
  }
};

When it comes to client-side rendering, react-document-title will just change document.title. If you wanted to add a default title to the pages in case no other component defines a title, you could do it at the <App /> level, changing that component as follows:

import React from 'react';
import {RouteHandler} from 'react-router';
import DocumentTitle from 'react-document-title';

export default class App extends React.Component {
  render () {
    return <DocumentTitle title='Pony Foo'>
      <RouteHandler />
    </DocumentTitle>
  }
}

Ta-da! That was pretty easy. You could also use react-doc-meta to do the same about <meta> tags.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (8)

DrMabuse wrote

Hi nice Article, did you have plans about a boilerplate ?