Building Isomorphic Webapps on the JVM with React.js and Spring Boot


This article demonstrates how to combine React.js with Java and Spring Boot in order to pre-render fully-fledged MVC-frontends on the JVM server without even thinking about Node.js.

Wait. Isomorphic Webapps? What the heck is that?

Isomorphic JavaScript apps are JavaScript applications that can run both client-side and server-side.The backend and frontend share the same code.

Traditionally webapps generate HTML on the server and send it over to the client. This has changed with the recent rise of client-side MVC frameworks such as Angular.js, Backbone.js or Ember. But generating HTML views on the client has both pros and cons. Isomorphic webapps try to close this gap by enabling you to use the same technologies for generating views both on the server and on the client.

React.js is a fresh new JavaScript library for generating views programmatically. React is not a complete MVC framework - it's the V in MVC, concentrating on creating and managing views by dividing the entire UI into components. Those React components can be rendered both on the client and on the server.

The Nashorn JavaScript Engine makes isomorphic webapps on the JVM possible. Nashorn as part of the latest Java 8 release dynamically translates JavaScript into bytecode so it runs natively on the JVM.

The rest of this article explains how to build isomorphic webapps with React on the JVM by utilizing Nashorn and Spring Boot to pre-render React views on the server. The example code is hosted on GitHub and focuses around the official React.js tutorial - a comment box example with markdown support. If you're not yet familiar with React.js, just follow the steps described here. You may also want to read my Nashorn Tutorial later, but it's not mandatory for this blog post.

React Views and Templates

The main React component as described in the official tutorial looks like this:

var CommentBox = React.createClass({
    // ...
    render: function () {
        return (
            <div className="commentBox">
                <h1>Comments</h1>
                <CommentList data={this.state.data} />
                <CommentForm onCommentSubmit={this.handleCommentSubmit} />
            </div>
        );
    }
});

In order to render this component in the browser we define a function renderClient which simply calls React.render to attach the view to the content container of the page:

var renderClient = function (data) {
    React.render(
        <CommentBox data={data} url='comments.json' pollInterval={5000} />,
        document.getElementById("content")
    );
};

Calling this function renders the comment box component into a pre-defined content div container by passing an array of comment objects as data. We cannot call this function on the server because it highly depends on the browser DOM being present (see document).

The great part about React.render is that it respects pre-rendered HTML from the server:

If you call React.render() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers, allowing you to have a very performant first-load experience.

So, the next step is to render the whole view on the server. We need two things to achieve that: (i) the pre-rendered html and (ii) the pre-processed JSON data as input for renderClient.

Let's define a template index.jsp with model attributes content and data. Content will be filled with the pre-rendered HTML of the comment box while data will be replaced with the JSON array of comments so React can initialize all the components props and states on page-load.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Hello React</title>
    <script type="text/javascript" src="react.js"></script>
    <script type="text/javascript" src="showdown.js"></script>
    <script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<div id="content">${content}</div>
<script type="text/javascript" src="commentBox.js"></script>
<script type="text/javascript">
    $(function () {
        renderClient(${data});
    });
</script>
</body>
</html>

You can certainly choose another templating library then JSP, e.g. Thymeleaf or Handlebars. Whatever you prefer...

Server-side rendering with Nashorn

Java 8 ships with a brand new JavaScript engine called Nashorn. You can simply create a new Nashorn engine by utilizing ScriptEngineManager.

NashornScriptEngine nashorn = (NashornScriptEngine)
    new ScriptEngineManager().getEngineByName("nashorn");
nashorn.eval(read("nashorn-polyfill.js"));
nashorn.eval(read("react.js"));
nashorn.eval(read("showdown.js"));
nashorn.eval(read("commentBox.js"));

The above code evaluates all scripts needed for the comment box tutorial. The helper method read simply reads a file from the classpath by creating a new io reader:

Reader read(String path) {
    InputStream in = getClass().getClassLoader().getResourceAsStream(path);
    return new InputStreamReader(in);
}

Unfortunately React doesn't evaluate properly on Nashorn without some fixes. I've created a file called nashorn-polyfill.js to address those issues (see this issue).

This is the content of this file:

var global = this;

var console = {};
console.debug = print;
console.warn = print;
console.log = print;

The following Java method demonstrates how to render the HTML of the comment box tutorial via Nashorn on the server:

String renderCommentBox(List<Comment> comments) {
    try {

        Object html = nashorn.invokeFunction("renderServer", comments);
        return String.valueOf(html);
    }
    catch (Exception e) {
        throw new IllegalStateException("failed to render react component", e);
    }
}

As you can see we directly pass a native Java list of comments as input data. We call the JavaScript function renderServer located in commentBox.js. It looks quite similar to renderClient as described above:

var renderServer = function (comments) {
    var data = Java.from(comments);
    return React.renderToString(
        <CommentBox data={data} url='comments.json' pollInterval={5000} />
    );
};

The function renderServer accepts a native Java list of comments as argument. Since the React components implemented in commentBox.js expect a javascript array, we have to convert the java list into the corresponding javascript array by calling Java.from. The function React.renderToString finally creates the desired view and returns an HTML string.

The Main Controller

Finally we wrap all things up into a Spring Boot controller. Remember that we need both model attributes content and data to properly render index.jsp. We just learned how to generate the content HTML with Nashorn. But we also need the initial JSON data so React knows about the components props and states. This is quite simple by utilizing Jacksons ObjectMapper to convert the list of comments into the appropriate JSON data.

@Controller
class MainController {
    CommentService service;
    React react;
    ObjectMapper mapper;

    @Autowired
    MainController(CommentService service) {
        this.service = service;
        this.react = new React();
        this.mapper = new ObjectMapper();
    }

    @RequestMapping("/")
    String index(Map<String, Object> model) throws Exception {
        List<Comment> comments = service.getComments();
        String content = react.renderCommentBox(comments);
        String data = mapper.writeValueAsString(comments);
        model.put("content", content);
        model.put("data", data);
        return "index";
    }
}

That's all. The main controller renders the HTML for all available comments on the server. React.render gets called in the browser on page-load, preserves all server-rendered markup, initializes the internal props and states of the components and registers all event handlers. The comment box gets auto-refreshed every couple of seconds and newly created comments will directly be attached to the DOM without waiting for the server to answer.

Isomorphic server-rendering in this example has many benefits compared to generating the views solely on the client: The page is fully search-engine optimized (SEO), so search engines such as Google can parse every comment properly. There's also no UI flickering on first page-load: Without server-rendering the browser first shows an empty page, then fetches all comments and renders the markup. Depending on the performance of the clients hardware you'll notice some flickering on startup. This is even more of an issue on mobile devices.

I hope you enjoyed reading this post. If you want to learn more about the Nashorn Engine you probably want to read my Nashorn Tutorial. The full source code of this example project is hosted on GitHub. Feel free to fork the repository or send me your feedback via Twitter.

Benjamin is Software Engineer, Full Stack Developer at Pondus, an excited runner and table foosball player. Get in touch on Twitter, and GitHub.

Read More