By Oleksii Rudenko October 30, 2016 3:23 PM
Universal Javascript with JSPM

Inspired by the release of next.js earlier this week, I decided to write a tutorial about implementing the same functionality with JSPM. As you may have noticed, next.js is using Webpack and JSPM is an alternative to Webpack that is based on SystemJS. So in this tutorial I will show how to create an universal JS app using the latest JSPM beta and achieve the following features of next.js:

  • Server side rendering
  • Automatic code splitting

So let’s start:

Installation

We will need to use the latest JSPM beta as it has better support for universal JS. First, create a directory for your project as follows:

mkdir universal && cd universal

Then install JSPM globally and initialize your JSPM project:

npm i jspm@beta -g
jspm init

Use the default answers for questions JSPM asks during initialization. The output would be like:

JSPM Universal JS initialization of a project

Next, let’s install all dependencies we will need:

jspm install react react-dom
jspm install --dev npm:babel-plugin-transform-react-jsx core-js # for JSX support
jspm install --dev npm:express # express as an http server

Once all of this has been installed, we need to finish configuring JSX. Open jspm.config.js and add babelOptions to the existing configuration as follows:

  packages: {
    "app": {
      "main": "app.js",
      "format": "esm",
      "meta": {
        "*.js": {
          "loader": "plugin-babel",
          "babelOptions": {
            "plugins": [
              "babel-plugin-transform-react-jsx"
            ]
          }
        }
      }
    }
  }

Now create a directory called src for the source code of the app and inside the src create a directory called pages with index.js inside. In this directory we will keep application pages (e.g. index.js, about.js). For example, index.js can contain:

import React from 'react';

export default class extends React.Component {
  onClick() {
    alert('On click');
  }

  render() {
    return <div>
        <h2>
          Hello World!
        </h2>
        <button onClick={this.onClick}>
          Click me
        </button>
      </div>;
  }
}

In the render function we have some static HTML tags and dynamic content such as onClick handler. Now let’s add a file called server.js in the root directory of our project. First, we will render the IndexPage without dynamic content:

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server.js';
import { IndexPage } from './pages/index.js';

let app = express();

app.get('/', (req, res) => {
  // just renders IndexPage to a string and wraps it with html, body, #container tags
  let pageHTML = ReactDOMServer.renderToString(React.createElement(IndexPage));
  let result = `
    <html>
      <head>
        <meta charset="utf-8">
      </head>
      <body>
        <div id="container">${pageHTML}</div>
      </body>
    </html>`;
  res.send(result);
});

app.listen('8080', () => console.log('Listening on port 8080'));

Once ready simply run jspm run server.js and you will see static content saying Hello World! and a button Click me. If you try to click the button though you will see that nothing happens.

JSPM Universal JS static rendering

Now what is left is to implement the state rehydration to load dynamic content (i.e. the onClick handler). For this we will need to inject the following Javascript code into the wrapping HTML:

<script src="jspm_packages/system.js"></script>
<script src="jspm.config.js"></script>
<script>
  Promise.all([
    System.import('./src/pages/index.js'), // load the page component
    System.import('react-dom'),
    System.import('react')
  ]).then((results) => {
    const IndexPage = results[0].default;
    const ReactDOM = results[1];
    const React = results[2];
    // render into existing container without removing already rendered DOM elements (rehydration)
    ReactDOM.render(React.createElement(IndexPage), document.getElementById('container'));
  });
</script>

The full server.js is as follows:

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server.js';
import IndexPage from './src/pages/index.js';

let app = express();

app.use('/jspm_packages', express.static('jspm_packages'));
app.use('/src', express.static('src'));
app.use('/jspm.config.js', express.static('jspm.config.js'));

app.get('/', (req, res) => {
  let pageHTML = ReactDOMServer.renderToString(React.createElement(IndexPage));
  let result = `
    <html>
      <head>
        <meta charset="utf-8">
      </head>
      <body>
        <div id="container">${pageHTML}</div>
        <script src="jspm_packages/system.js"></script>
        <script src="jspm.config.js"></script>
        <script>
          Promise.all([
            System.import('./src/pages/index.min.js'),
            System.import('react-dom'),
            System.import('react')
          ]).then((results) => {
            const IndexPage = results[0].default;
            const ReactDOM = results[1];
            const React = results[2];
            ReactDOM.render(React.createElement(IndexPage), document.getElementById('container'));
          });
        </script>
      </body>
    </html>`;
  res.send(result);
});

app.listen('8080', () => console.log('Listening on port 8080'));

Now just restart the server and open the page again. Give it some time to fetch JS files and click the button. You will see that the alert dialog appears. If you run this tutorial on your machine, you will also notice that it takes quite some time to load JS after the static content has been rendered. The reason is that JSPM does not bundle dependencies by default and even transpilation happens in the browser. This allows you to achieve true automatic code splitting but comes at a cost of the degraded performance. So for using it in production, you will need to explore bundling/building/CDN/HTTP2 capabilities of JSPM.

Find the full source code here: https://github.com/OrKoN/jspm-universal-js-demo

Thanks for reading!