Server-Side Rendering with Universal JavaScript for Relay Applications

February 27, 2017
Rachel Lee

Server-side Rendering with JavaScript

For the entire history of the internet, server rendered web pages have been the de facto standard (e.g. ASP, JSP, PHP). Server rendered HTML generally provides the best SEO support and lowest time to first render. Recently, client-side rendering frameworks such as React and Angular have become powerful tools for building interactive, component-based web applications. With this progress, it’s becoming popular to leverage these frameworks in a way that reaps the benefits of both server-side and client-side rendering. This approach is called “Universal JavaScript”.

Think carefully about whether you need server-side rendering. If you don’t really need it I wouldn’t recommend implementing it, even if it is all the rage these days.

If you’re not sure whether you need server-side rendering (SSR), here’s my quick list:

DO use server-side rendering…

  • if you need SEO on non-Google search engines
  • if you have a large application and need to reduce time to first render for UX

DO NOT use server-side rendering…

  • if you’re ok with just Google SEO
  • if you have very limited server resources (only applies to SSR with JavaScript)

If you’re looking for more info about server-side rendering with JavaScript, you can check herehere, or here.

This article is focused on developing a universal GraphQL/Relay application in particular. If that’s your situation, great! If not, I highly recommend you check out GraphQL. If you decide GraphQL fits your needs, you can get started with Choosing a GraphQL Client.

Using isomorphic-relay-router

If all you wanted to see was code, here. This is a sample full-stack Relay project using isomorphic-relay-router. It’s available on GitHub to serve as an example for the rest of this post.

A note about the terminology: This module uses “isomorphic” in its name, but it’s now largely considered a legacy term. Instead, “universal” is more commonly used to describe code that runs on both the server and the client. You can find more information about the terminology and history of universal JavaScript in this quick article.

There’s really not much that has to change in order to use isomorphic-relay-router on top of a Relay app using react-router-relay. It’s fairly similar to upgrading a plain React app using react-router to a universal React app. Here’s the general process:

  1. Install the dependencies: ejs, isomorphic-relay, and isomorphic-relay-router.
    • isomorphic-relay allows us use Relay universally
    • isomorphic-relay-router allows us to render Relay components on both the server and client
    • ejs is our server-side template rendering engine
  2. Let’s start with setting up the template (with ejs) for our server to render. If we had an index.html file, or equivalent, we can remove that now. Our index.ejs file will look almost the same as the html file except for the parts that load the server-rendered react component and the data.
    <!DOCTYPE html>
    <html>
      <head>
        <title>React-Relay-GraphQL</title>
        <link rel="stylesheet" href="styles.css">
      </head>
      <body>
        <div id="root">
          <div><%- reactOutput %></div>
        </div>
        <script id="preloadedData" type="application/json">
          <%- JSON.stringify(preloadedData).replace(/\//g, "\\/") %>
        </script>
        <script src="bundle.js"></script>
      </body>
    </html>
  3. If your react-router routes aren’t already in a separate file, now would be a great time to move them. We need our routes to live in a separate file because they’ll be needed both server and client side in order to render the correct component.
  4. In the server/index.js file that holds the endpoints for our app, we need to add the isomorphic route. We specify the network layer for Relay, use react-router’s match function. If you’re not familiar with server-side rendering with react-router, you can find a little more info on their docs.
    import IsomorphicRouter from 'isomorphic-relay-router';
    import ReactDOMServer from 'react-dom/server';
    import { match } from 'react-router';
    import Relay from 'react-relay';
    import routes from '../app/routes';
    
    app.use((req, res, next) => {
      // must use absolute url for network layer
      const networkLayer = new Relay.DefaultNetworkLayer(
        `http://localhost:${PORT}/graphql`,
      );
    
      match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
        const render = ({ data, props }) => {
          const reactOutput = ReactDOMServer.renderToString(
            IsomorphicRouter.render(props),
          );
          res.render(path.resolve(__dirname, 'views', 'index.ejs'), {
            preloadedData: data,
            reactOutput,
          });
        };
    
        if (error) {
          next(error);
        } 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');
        }
      });
    });
  5. Lastly, we need to set up the client side to receive the server-rendered template with data. Again, we set the network layer for Relay, use react-router’s match function to render the right component, and render with ReactDOM this time.
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Relay from 'react-relay';
    import { browserHistory, match, Router } from 'react-router';
    import IsomorphicRelay from 'isomorphic-relay';
    import IsomorphicRouter from 'isomorphic-relay-router';
    import routes from './routes';
    
    const environment = new Relay.Environment();
    // match network layer with server's isomorphic network layer
    environment.injectNetworkLayer(new Relay.DefaultNetworkLayer('/graphql'));
    
    const preloadedData: any = document.getElementById('preloadedData');
    const data = JSON.parse(preloadedData.textContent);
    IsomorphicRelay.injectPreparedData(environment, data);
    
    match({ routes, history: browserHistory }, (error, redirectLocation, renderProps) => {
      IsomorphicRouter.prepareInitialRender(environment, renderProps).then((props) => {
        ReactDOM.render(<Router {...props} />, document.getElementById('root'));
      });
    });
    
  6. Congratulations! That’s it. The app should still function the same as before. As a sanity check, you can see if the sources look like what you expect in the developer console of your favorite browser. In my example, there’s the index file that was rendered by the server using the ejs template.
    universal relay screenshot

Coda

Server-side rendering with Relay is super exciting and interesting, but it’s not for everyone. This is a very specific example of universal JavaScript, and I truly look forward to the future of server-side rendering. Relay doesn’t come with server-side rendering out-of-the-box as of now, but there may be plans to include it as a feature in Relay 2. You can keep up with some Relay 2 updates via the GitHub issue.

For more info about the starter kit used as an example in this post, check out my colleague’s article on Strategies for Creating a Web Application Starter Kit.

— Posted by Rachel Lee, Software Engineer

About the Author

Rachel studied Computer Science and Cognitive Science at UC Berkeley and is currently working for Codazen as a software engineer.

She holds festive apple pie baking parties and has a big cheeky dog that snores and likes to nap on people’s feet.

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Get More. Subscribe to our Technical Blog.

SUBSCRIBE