header

Universal/Isomorphic React on a Go backend

React’s standard flow is to start with an empty HTML template from the server, and populate the DOM entirely on the client. Letting React pre-render the initial HTML for your app on the server before the client takes over has some advantages, though: it lets slower (mobile) clients load your app much faster, and it’s good for SEO for search engines that don’t support JavaScript. Since you have to run React on the server to do this, you need a JavaScript-able backend, which is why such Universal (a.k.a. Isomorphic) apps are mostly built with Node.js backends. However, you can create universal React apps with other languages too, including Go. In this post, I’ll walk through an example of a universal React app running on a Google App Engine Standard Go backend (which is slightly more constrained than a regular Go server).

The Client JavaScript App

Because Redux lends itself well as the basis of a universal React data model, we’ll take the Redux Counter Example as the basis of our app. I will leave out the unimportant parts in this post; you can always find the full example in the Git repository.

The main client.js file of the Counter app fetches the state via AJAX from the server on the /api/state endpoint, and renders the Counter component:

import { createStore } from 'redux';
import { Provider } from 'react-redux';
import Counter from './Counter';
import { rootReducer } from './reducers';

function load(state) {
  const store = createStore(rootReducer, state);

  // Render our main component
  render(
    <Provider store={store}>
      <Counter/>
    </Provider>,
    document.getElementById('root')
  );
}

fetch('/api/state').then(
  response => response.json().then(load),
  err => console.error(err)
);

On the server side, we have a handler for /api/state that gets the state from the database, and returns it as a JSON object:

func init() {
  http.Handle("/api/state", handler(handleState))
}

func handleState(w http.ResponseWriter, r *http.Request) error {
  state, err := getStateFromDB(appengine.NewContext(r))
  if err != nil { return err }
  w.Header().Set("Content-Type", "application/json")
  return json.NewEncoder(w).Encode(state)
}

type State struct {
  Value int `datastore:"value,noindex" json:"value"`
}

func getStateFromDB(ctx appengine.Context) (*State, error) {
  var state State
  key := datastore.NewKey(ctx, "State", "currentState", 0, nil)
  if err := datastore.Get(ctx, key, &state); err != nil {
    return nil, err
  }
  return &state, nil
}

For now, all we need is to serve a static index.html at the root to get the app running:

<!doctype html>
<html>
  <head>
    <title>React+Go Test App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/js/app.js"></script>
  </body>
</html>

This gives us a regular React app, where all the rendering is dynamically happening on the client, and the server only provides the data API.

Pre-rendering the App on the Server

To pre-render our app on the server, we start by creating a helper JavaScript render function, which we will call from the server:

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { default as genericRender } from './render';

export function render(preloadedState) {
  const store = createStore(rootReducer, JSON.parse(preloadedState));
  return {
    html: renderToString(
      <Provider store={store}>
        <Counter/>
      </Provider>
    ),
    state: JSON.stringify(store.getState())
  };
}

The render function renders the same component as client.js, except it uses React’s renderToString to build a string instead of injecting it in the DOM. Next to the rendered string, we also return the state from the Redux store (which, apart from populated default values, will be the same as preloadedState), so we can pass it on to the client. We can then bundle this function and all its dependencies (including React and Redux) up into a single server.js JavaScript file using webpack.

To run this code on the server, we can use Otto, a JavaScript interpreter written in pure Go (which means it can run on Google App Engine Standard). Using Otto, we load the server.js bundle, and call the render function with the data we get from the database.

var templates = template.Must(template.ParseFiles("index.html"))

func init() {
  http.Handle("/", handler(handleIndex))
}

func handleIndex(w http.ResponseWriter, r *http.Request) error {
  state, err := getStateFromDB(appengine.NewContext(r))
  if err != nil { return err }
  var renderedHTML, renderedState string
  renderedHTML, renderedState, err = render(state)
  if err != nil { return err }
  return templates.ExecuteTemplate(w, "index.html", IndexPage {
    HTML:  template.HTML(renderedHTML),
    State: template.JS(renderedState),
  })
}

// Call render.js's render() function with the given state, 
// and return the resulting HTML and the initialized state
func render(state *State) (string, *State, error) {
  stateJSON, err := json.Marshal(state)
  if err != nil { return "", "", err }
  var renderResult otto.Value
  renderResult, err = callRenderJS(string(stateJSON))
  if err != nil { return "", nil, err }

  var renderedHTML, renderedState otto.Value
  renderedHTML, err = renderResult.Object().Get("html")
  if err != nil { return "", nil, err }
  renderedState, err = renderResult.Object().Get("state")
  if err != nil { return "", nil, err }

  return renderedHTML.String(), renderedState.String(), nil
}

// Compile render.js, and call the resulting 'render' function 
// on the passed state
func callRenderJS(stateJSON string) (otto.Value, error) {
  vm := otto.New()

  var v, renderJS otto.Value
  script, err := vm.Compile("server.js", nil)
  if err != nil { return v, err }
  v, err = vm.Run(script)
  if err != nil { return v, err }
  v, err = vm.Get("server")
  if err != nil { return v, err }
  renderJS, err = v.Object().Get("render")
  if err != nil { return v, err }

  return renderJS.Call(otto.NullValue(), stateJSON)
}

The root handler now has to render a dynamic version of index.html with the pre-rendered html and current state embedded, so that Redux can continue on the client where the server left off:

<!doctype html>
<html>
  <head>
    <title>React+Go Test App</title>
  </head>
  <body>
    <div id="root">{{.HTML}}</div>
    <script>
      window.__PRELOADED_STATE__ = {{.State}};
    </script>
    <script src="/js/app.js"></script>
  </body>
</html>

Finally, in client.js, we use the preloaded state from __PRELOADED_STATE__ to hydrate our Redux store, and render the prerendered HTML using React.hydrate.

import { hydrate } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import Counter from './Counter';
import { rootReducer } from './reducers';

function load(state) {
  const store = createStore(rootReducer, state);
  hydrate(
    <Provider store={store}>
      <Counter/>
    </Provider>,
    document.getElementById('root')
  );
}

load(window.__PRELOADED_STATE__);

And that’s it! Whenever a client accesses our app, the server will pre-render the HTML using data from the database, and send it to the client. On the client-side, React will detect that the contents has already been rendered, and not do any extra initial work.

JavaScript Development

In the server code above, render() reloads the JavaScript on every access. This means we can run webpack --watch on the JavaScript code while developing, and a page reload will run the newest version of server.js every time.

For development, webpack also has a very convenient dev server mode, which compiles and serves all code from memory, and reloads your page on every change. On top of this, it can even hot-load your CSS and React modules on the fly, without any page reload at all. Since webpack’s dev-server mode works entirely from memory and doesn’t write anything to disk, we can’t rely on the server to have the latest version of server.js, and so the pre-rendered code can be inconsistent with the client code. To make sure this development model work as well, we can tell webpack’s proxy to always add an X-DevServer header when forwarding requests to the backend; in the backend, we can then skip the pre-rendering by sending an empty html and state, and let the client do all the rendering itself again:

func handleIndex(w http.ResponseWriter, r *http.Request) error {
  if len(r.Header["X-Devserver"]) > 0 {
    // When accessing through the dev server, 
    // don't pre-render anything
    return templates.ExecuteTemplate(w, "index.html", IndexPage {
      HTML:  "",
      State: template.JS("null"),
    })
  } else {
    // Prerender
    ...
  }
}

On the client, we now just have to check whether the server gave us pre-rendered state, and otherwise fetch the state via AJAX:

function load(state, isPreload) {
  const store = createStore(rootReducer, state);
  (isPreload ? hydrate : render)(
    <Provider store={store}>
      <Counter/>
    </Provider>,
    document.getElementById('root')
  );
}

// Check if we can preload the state from 
// a server-rendered page
if (window.__PRELOADED_STATE__) {
  load(window.__PRELOADED_STATE__, true);
}
else {
  // We didn't pre-render on the server, 
  // so we need to get our state
  fetch('/api/state')
    .then(
      response => response.json().then(load),
      err => console.error(err)
    );
}

JavaScript Debugging

A downside with rendering JavaScript on the server is that, when your JavaScript code goes bad, it can be tricky to track down where. Doing all your development on the client definitely helps here, but sometimes you can still hit a bug only when rendering on the server (e.g. when you’re passing unexpected state to render in production). Since the render function is a pure function that translates a state object to HTML, you can log the state on the server as a JSON string when an exception occurs, and run the JSON string offline through a simple command-line Node.js script renderComponent to get decent stacktraces or do some debugging:

// Example: 
//  ./renderComponent '{"value": 42}'

var server = require('./JS/server');

if (process.argv.length < 3) { 
  throw Error("Missing arguments"); 
}

console.log(
  server.render(process.argv[2]).html);

Caching for Production

When running the server in production, recompiling server.js as is done above is a useless waste of time. By factoring out the compilation process from callRenderJS(), you can cache the compiled server.js when initializing the server. For more details, see the full example source code.

Conclusion

You don’t need a Node.js backend to be able to build a universal React app. In this post, I used Go and Otto to build such an example app (for which you can find the complete sources in my Git repository). However, you can follow the same pattern on other languages, including Swift (which I described in a previous post), Java (using the Nashorn JS engine) or any language with native bindings, using e.g. V8 (or even duktape!).

Published by

Remko Tronçon

Software Engineer · Hobby musician · BookWidgets