redux

Server Side Rendering with Spring Boot and React

Server Side Rendering with Spring Boot and React

Server side rendering of React apps in Spring Boot is supported using Spring’s script template views. Although still young, script template views provide the foundation for building isomorphic React apps. I will show you how this is done using React’s server side rendering capabilities and later I will explain how to add support for React Router and Redux.

In a previous post I wrote a little over a year ago, I covered the very basics of Spring’s script template views with a small app that used EJS script templates. If you followed a link from somewhere expecting that post, you’ve been redirected here because this post will be much more complete.

You can find the full source of the sample application I built on my GitHub repo. If you’d like to see an alternative to JSR-223, be sure to check out the J2V8 branch of the project. The J2V8 version performs better than the Nashorn version.

Basic Setup

The basic setup is still the same. First I’ve defined a ViewResolver typed bean which returns a new ScriptTemplateViewResolver with prefix of /public/ and suffix of .html. Next I’ve defined a ScriptTemplateConfigurer bean. The recommended approach is to instruct Spring to create a non-shared ScriptEngine when using libraries like React that are not designed for concurrency.

Lastly, I’ve defined a couple of scripts that are on the classpath named polyfill.js and server.js, and told Spring that the render method is called simply render.

@Bean
public ViewResolver viewResolver() {
    return new ScriptTemplateViewResolver("/public/", ".html");
}

@Bean
public ScriptTemplateConfigurer scriptTemplateConfigurer() {
    ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
    configurer.setEngineName("nashorn");
    configurer.setScripts(
            "static/polyfill.js",
            "public/server.js"
    );
    configurer.setRenderFunction("render");
    configurer.setSharedEngine(false);
    return configurer;
}

Handling Requests

Handing requests to render the script templates is done by creating a root or index controller. I can’t simply setup a root @GetMapping or @GetMapping("/") because that will intercept the client side assets. When requests come in for client.js or client.css for example, the response body would be the contents of a script template. To circumvent this, the get mapping uses a regex negative lookahead to ensure it does not match against any static assets. It’s certainly not a very friendly solution, but it works for this example.

@Controller
puglic class IndexController
    @GetMapping("/{path:(?!.*.js|.*.css|.*.jpg).*$}")
    public String index() {
        return "index";
    }
}

Server Side Scripts

First is the server.js script which will be built using Webpack and written to src/main/resources/public. Using a bundling utility like Webpack makes it easier to use libraries like React in server side rendering, because you don’t have to tell Nashorn about them when initializing a script engine.

Using renderToString from ReactDOM, I can render the App component server side. I’ve attached the render function to window which will make it visible to the script engine when it needs to render the templates. The result will be injected into the template where the SERVER_RENDERED_HTML placeholder exists and the final result of the render function will be written to the view.

import React from 'react';
import {renderToString} from 'react-dom/server';
import App from 'components/App';

window.render = (template) => template.replace('SERVER_RENDERED_HTML', renderToString(<App/>));

Next is the polyfill.js script which I’ll place in src/main/resources/static so that it doesn’t get clobbered by the Webpack build. This script doesn’t need to ever get rebuilt, it’s simply there to provide alternatives to variables that are not available on the server.

var window = this;
var console = {
  error: print,
  debug: print,
  warn: print,
  log: print
};
window.setTimeout = function() {};
window.Promise = {
  resolve: function() {},
  reject: function() {}
};

The Template

The next piece of the puzzle is a template called index.html. This sample shows the result of a Webpack build. The template has been written to src/main/resources/public and the static assets have been injected. What’s important to take note of is that these assets are being resolved to an absolute path. No matter what page gets visited, they will always be resolved from the root path and not relative. Also worth mentioning is the SERVER_RENDERED_HTML placeholder that I spoke about earlier. This is where the result of the server side render function will be injected.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="/client.css?9486db0c3127751c07bf" rel="stylesheet"></head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->

<div id="app">SERVER_RENDERED_HTML</div>

<script type="text/javascript" src="/client.js?9486db0c3127751c07bf"></script></body>
</html>

Rendering Client Side

The client.js script is very similar to the server.js script. The main difference is that it uses the browser side render function from ReactDOM and renders the App inside an actual HTML element.

import React from 'react';
import {render} from 'react-dom';
import App from 'components/App';

render(<App/>, document.getElementById('app'));

Let’s Add Routing

This is where the benefits of server side rendering become more obvious. I’m using React Router 4, which is still in alpha stage, but works for this example. I tried really hard to make this work with React Router 3, but I kept hitting one road block after the next. React Router 4’s major implementation change is that it’s now more focused on being component based and embracing the React way of doing things. You can read more about why here.

Let’s start by installing React Router 4 with the command npm install --save [email protected].

Populate The Model

In the index request mapping, I need to add the model information which will be made available to the server side render function when the view is resolved. The only thing I’m adding is a Java Map which is serialized to a JSON encoded string using Jackson’s ObjectMapper.

Contained inside the map is a single key named location which maps to the servlet path from the request, as well as any query string parameters. This value will be used by the server render function to initialize the ServerRouter.

@GetMapping("/{path:(?!.*.js|.*.css|.*.jpg).*$}")
public String index(Model model, HttpServletRequest request) throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();
    Map<String, Object> req = new HashMap<>();

    String root = request.getServletPath().equals("/index.html") ? "/" : request.getServletPath();

    if(request.getQueryString() != null)
        req.put("location", String.format("%s?%s", root, request.getQueryString()));
    else
        req.put("location", root);

    model.addAttribute("req", mapper.writeValueAsString(state));
    return "index";
}

Revisiting the Server Side Render Function

The server.js script requires some additional instrumentation to support React Router. The main difference is that the render function now accepts a second argument containing a model that I just populated in the index request mapping. I will retrieve the location property and use it to initialize the ServerRouter component. The result will be injected into the template at the same placeholder as before. The location will get passed down the component tree and any child routing components looking for that specific location will render.

import React from 'react';
import {renderToString} from 'react-dom/server';
import {ServerRouter, createServerRenderContext} from 'react-router';
import App from 'components/App';

window.render = (template, model) => {
  const context = createServerRenderContext();
  const req = JSON.parse(model.get('req'));
  const markup = renderToString(
    <ServerRouter location={req.location} context={context}>
      <App/>
    </ServerRouter>
  );
  return template.replace('SERVER_RENDERED_HTML', markup);
};

Time to Update the Client

The client.js script needs to be changed as well. It’s still quite similar to the server.js script, but uses a BrowserRouter instead of a ServerRouter. I don’t need to initialize any location this time because it will listen to the browser URL to determine what to render when the client side takes over.

import React from 'react';
import {render} from 'react-dom';
import {BrowserRouter} from 'react-router';
import App from 'components/App';

const markup = (
  <BrowserRouter>
    <App/>
  </BrowserRouter>
);

render(markup, document.getElementById('app'));

Preloading State with Redux

Now that the app is capable of rendering server side and can handle routing as well, it’s time to add preloaded state to the mix using Redux on the server. To get things started, I’ve added Spring Data JPA and Spring Data REST to the project. I’ve also added an H2 in-memory database and setup an Item entity with a REST repository providing HAL formatted output. Be sure to refer to my GitHub repository as I’ll only be showing key elements here.

I’ve setup the REST repository with a basePath of /api. In order to prevent the index request mapping from intercepting those calls, I need to update the @GetMapping. You’ll also see that I’m adding a new model attribute named initialState which contains some data from the item repository.

@GetMapping("/{path:(?!.*.js|.*.css|.*.jpg|api).*$}")
public String index(Model model, HttpServletRequest request) throws JsonProcessingException {
  ObjectMapper mapper = new ObjectMapper();

  Map<String, Object> req = new HashMap<>();
  String root = request.getServletPath().equals("/index.html") ? "/" : request.getServletPath();
  if(request.getQueryString() != null)
    req.put("location", String.format("%s?%s", root, request.getQueryString()));
  else
    req.put("location", root);
  model.addAttribute("req", mapper.writeValueAsString(req));

  Map<String, Object> initialState = new HashMap<>();
  initialState.put("items", itemRepository.findAll());
  model.addAttribute("initialState", mapper.writeValueAsString(initialState));
  return "index";
}

Rendering the Preloaded State on the Server

In order to preload the state in Redux and maintain server side rendering, I will create a store and pass it the initial state from the model. The markup is only slightly different, there’s now a top level Provider component which receives the store. Lastly, I’m injecting the initial state into the template using the SERVER_RENDERED_STATE placeholder.

When I first published this post, I was using JSON.stringify(initialState) to replace SERVER_RENDERED_STATE. After reading an interesting post on how this introduces an XSS vulnerability, I’ve updated it to use serialize-javascript.

import React from 'react';
import {renderToString} from 'react-dom/server';
import {ServerRouter, createServerRenderContext} from 'react-router';
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import {Provider} from 'react-redux';
import serialize from 'serialize-javascript';
import reducer from './reducers';
import App from 'components/App';

window.render = (template, model) => {
  const context = createServerRenderContext();
  const req = JSON.parse(model.get('req'));
  const initialState = JSON.parse(model.get('initialState'));

  const store = createStore(reducer, initialState, applyMiddleware(thunkMiddleware));

  const markup = renderToString(
    <Provider store={store}>
      <ServerRouter location={req.location} context={context}>
        <App/>
      </ServerRouter>
    </Provider>
  );

  return template
    .replace('SERVER_RENDERED_HTML', markup)
    .replace('SERVER_RENDERED_STATE', serialize(initialState, {isJSON: true}));
};

Adding Preloaded State to the Template

The only thing I need to do in the template is define a script at the end of the body which defines the __PRELOADED_STATE__ variable who’s value will be injected using the SERVER_RENDERED_STATE placeholder.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title></title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->

<div id="app">SERVER_RENDERED_HTML</div>
<script>
  window.__PRELOADED_STATE__ = SERVER_RENDERED_STATE;
</script>

</body>
</html>

Rendering the Preloaded State on the Client

Yet again, the client is similar to the server. It also wraps the existing markup with a Provider component that receives an Redux store. The main difference is where the preloaded state comes from. This time it comes from the __PRELOADED_STATE__ variable that I just defined in the template.

import React from 'react';
import {render} from 'react-dom';
import {BrowserRouter} from 'react-router';
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import {Provider} from 'react-redux';
import reducer from './reducers';
import App from 'components/App';

const store = createStore(reducer, window.__PRELOADED_STATE__, applyMiddleware(thunkMiddleware));

const markup = (
  <Provider store={store}>
    <BrowserRouter>
      <App/>
    </BrowserRouter>
  </Provider>
);

render(markup, document.getElementById('app'));

Conclusion

That’s it! This was quite a learning experience for me. I tried several things along the way that failed, but never lost sight of my goal. The index request mapping saw several iterations, because I’m not a regex expert, but who is? Perhaps Spring even offers a better approach that I’m ignorant of.

I also spent a lot of time trying to make this work with React Router 3, which ultimately proved to be unfruitful. Adding Redux was pretty straight forward with a little guidance from their documentation. As always, I welcome any feedback. If you think this post needs some improvements, feel free to comment.

Posted by Patrick Grimard in Programming, 0 comments