header

Universal/Isomorphic React on a Swift 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 Swift. In this post, I’ll walk through an example of a universal React app using Swift’s Vapor web framework.

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 Vapor handler for /api/state that gets the state from the database, and returns it as a JSON object:

import Vapor

// Dummy database
func getStateFromDB() -> [String: Any] {
  return [ "value": 42 ]
}

let drop = Droplet()

drop.get("/api/state") { req in
  return toJSON(value: getStateFromDB())!
}

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+Swift 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 = undefined) {
  const store = createStore(rootReducer, 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 Swift’s built-in JavaScriptCore1 framework to load the server.js bundle, and call the render function with the data we get from the database.

func loadJS() -> JSValue? {
  let context = JSContext()
  do {
    let js = try String(
      contentsOfFile: "server.js",
      encoding: String.Encoding.utf8)
    context?.evaluateScript(js)
  } 
  catch (let error) {
    return nil
  }
  return context?.objectForKeyedSubscript("server")
}

// Dummy
func getStateFromDB() -> [String: Any] {
  return [ "value": 4 ]
}

drop.get("/") { req in
  let state = getStateFromDB()
  if let result = loadJS()?.forProperty("render")?
      .call(withArguments: [state]).toDictionary() {
    return try drop.view.make("index", [
      "html": Node.string(result["html"] as! String),
      "state": Node.string(result["state"] as! String)
    ])
  }
  throw Abort.badRequest
}

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+Swift 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.

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(
    <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, loadJS() 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. 2

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:

drop.get("/") { req in
  if req.headers["X-DevServer"] != nil {
    // When accessing through the dev server, 
    // don't pre-render anything
    return try drop.view.make("index", [
      "html": "",
      "state": "undefined"
    ])
  }
  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:

// Check if we can preload the state from 
// a server-rendered page
if (window.__PRELOADED_STATE__) {
  load(window.__PRELOADED_STATE__);
}
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(JSON.parse(process.argv[2])).html);

Conclusion

You don’t need a Node.js backend to be able to build a universal React app. In this post, I used Swift, Vapor, and JavaScriptCore 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 Java (using the Nashorn JS engine) or any language with native bindings, using e.g. V8 (or even duktape!).


  1. Unfortunately, JavaScriptCore isn’t available for Linux. As a replacement, I used Duktape with a Duktape binding for Swift on this platform

  2. In my experiment, JavaScriptCore’s interpreter was fast enough to not have to cache anything. With larger apps in production, you’ll probably want to either only load the JavaScript code once, or check a timestamp to see whether the file needs to be reloaded.

Published by

Remko Tronçon

Software Engineer · Hobby musician · BookWidgets