Build a React Application from First Principles - Modules with SystemJS

This is Part 8 of the series - Build a React Application from First Principles.

Get the completed code from the last step on GitHub and follow along!

We did a lot in our last article, but our HTML file is starting to get quite long already, even though we only have two forms and a few buttons.

As we add more and more elements, the file would get too long. It would be much better to separate our components into separate files, where each file contains only one element.

Modularization

This separation is called modularization, as we are separating our features and functionality into distinct modules. This separation is not simply a stylistic choice, and have many benefits:

  • It'll be easier to find the relevant code since all code relevant to that component would be inside that file. If you give your files meaningful names, it would be easier to find than searching through 5000 lines of code.
  • Modules allow you to expose a limited set of exports (encapsulation) - this means if you have some internal utility functions that are not relevant elsewhere, you shouldn't export it and it will not be available outside the module.

To see a more in-depth demonstration of the benefits of modules, check out our other article - Why Use Modules?

Module Patterns

There are many ways to define modules. Addy Osmani has outlined many module patterns in his book Learning JavaScript Design Patterns, which is available online for free.

A bit of history - a few years ago, the Object Literal, Module and Revealing Module Patterns were popular, but nowadays there are four major module definition specifications:

You can get to know the differences between the module definition specifications in our other series - Working with Modules in JavaScript. For now, here's an over-generalized version:

  • CommonJS modules load their dependencies synchronously. This means code execution would halt until a package's dependencies are fully-loaded. If the dependency tree is large and heavily nested, loading time will be long. For this reason, CommonJS modules are traditionally used on the server, inside a NodeJS application, where the packages are downloaded once in advanced.

    CommonJS modules would not work well in the browser, as blocking downstream code execution whilst a library tries to resolve dependencies would be terrible for user experience.

    As we shall see later, however, tools like Webpack allows us to use CommonJS modules in the browser by resolving dependencies in advance, and bundling them all together into only one, or a few, files.

  • AMD modules load their dependencies asynchronously, which means it will not block downstream code execution. For this reason, dependencies are able to be loaded in parallel, reducing the load time of the application. The package code resides in a callback, which only runs after all the dependencies have been loaded.

  • UMD modules are similar to AMD modules, but contains more boilerplate which makes it compatible with CommonJS, as well as exposing the library into the global scope when used outside a module system.

  • ECMAScript 2015 (ES6) modules are the new standard syntax for defining modules and should be adopted.

How CommonJS came to dominate

It'd appear that ES6 modules should be the standard syntax, but the reality is that CommonJS still dominates the module definition syntax for modules. Why? To figure this out, we need talk a little about the modern history of JavaScript.

It'd be fair to say that the JavaScript ecosystem was growing steadily (and quietly) until NodeJS burst onto the scene. NodeJS allows developers to write JavaScript that runs on the server. This is revolutionary as developers can now write, using the same language, both frontend and backend code.

But before NodeJS became the de facto server-side JavaScript environment, there was, as with most things, a war between the different implementations. RingoJS is built on the JVM (Java Virtual Machine) and Rhino, Node was built on libev and V8. There were others like the now-deprecated Narwhal as well. So developers wanted a module definition specification, so modules written for Ringo would work on Node, and vice versa. CommonJS was the specification which was agreed upon.

Spoilers alert! NodeJS won that war, and soon developers are writing many modules and publishing them to npm, the most popular package repository for NodeJS, similar to Packagist for PHP (technically Composer) packages, or GoDoc for Go packages.

And because NodeJS used the CommonJS specification, the vast majority of JavaScript modules nowadays is defined as CommonJS modules.

AMD modules, although technically superior with their asynchronous loading, were pushed aside by this Node/npm onslaught.

ES6 modules are catching on, since it is standardized by a major party, but you'll still see a lot more requires than you'll see imports!

Splitting Code into Modules

History lesson over! Now let's take a look at how to split our components into modules.

First, create a new components directory, and create a .jsx file for each component.

$ mkdir components; cd components; touch RegistrationForm.jsx LogInForm.jsx LogOutButton.jsx Header.jsx App.jsx

Our directory looks like this now:

$ tree
.
├── components
│   ├── App.jsx
│   ├── Header.jsx
│   ├── LogInForm.jsx
│   ├── LogOutButton.jsx
│   └── RegistrationForm.jsx
├── img
│   └── logo.png
├── index.html
├── LICENSE
└── README.md

Next, copy the code for each component into their corresponding files. Add require statements whenever you need to import a component from another module. For example, Header.jsx would look like this:


var LogOutButton = require('./LogOutButton.jsx');

var Header = function (props) {
  return (
    <div>
      <img src="./img/logo.png" />
      {
        props.userIsLoggedIn ? <div><p>You're logged in!</p><LogOutButton/></div> : <p>Register or log In below</p>
      }
    </div>
  )
}

module.exports = Header;

We should be left only with this line inside our <script> tag of our index.html.

<script type="text/babel">  
  ReactDOM.render(<App />, document.getElementById('renderTarget'));
</script>  

Loading Modules in Browser

Now, how do we tell the browser to load our components into our application?

First, we'd need to make our components bind to the global object window, which makes it available everywhere. For example, the first line of LogOutButton.jsx would look like this:

window.LogOutButton = React.createClass({

Do this for every component.

Next, we need to include those components into our index.html.


  <script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
  <script type="text/babel" src="/components/LogOutButton.jsx"></script>
  <script type="text/babel" src="/components/RegistrationForm.jsx"></script>
  <script type="text/babel" src="/components/LogInForm.jsx"></script>
  <script type="text/babel" src="/components/Header.jsx"></script>
  <script type="text/babel" src="/components/App.jsx"></script>
</head>

Make sure the order of the <script> tags are maintained, as the Header element requires that the LogOutButton element be available when it's loaded.

And since we are including JavaScript files into our index.html, we need a web server to serve those files.

Luckily for us, most system have python installed, and python provides us with a simple HTTP server.

$ cd /path/to/grapevine/
$ python -m SimpleHTTPServer 8000

You can use another port if you want.

Now, we can load our application on localhost:8000.

$ python -m SimpleHTTPServer 8000
Serving HTTP on 0.0.0.0 port 8000 ...  
127.0.0.1 - - [06/Jan/2017 14:30:04] "GET / HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:05] "GET /components/LogOutButton.jsx HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:05] "GET /components/RegistrationForm.jsx HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:05] "GET /components/LogInForm.jsx HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:05] "GET /components/Header.jsx HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:05] "GET /components/App.jsx HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:06] "GET /img/logo.png HTTP/1.1" 200 -  
127.0.0.1 - - [06/Jan/2017 14:30:06] code 404, message File not found  
127.0.0.1 - - [06/Jan/2017 14:30:06] "GET /favicon.ico HTTP/1.1" 404 -  

You should see the same screen as we had before, but now everything is more modularized.

Converting to CommonJS

Great! But this form of module system is not ideal. Remember, we want to use the CommonJS specification so our module can work with other CommonJS modules.

In CommonJS, we include/import/require other modules by using require('module_name'), and we export variables to be exported using module.exports.

So let's convert our modules accordingly. Using LogOutButton as an example again, remove the window. (as we are no longer putting our components into the global scope) and add a modules.exports statement at the bottom.


window. var LogOutButton = React.createClass({
  handleLogOut: function () {
    firebase.auth().signOut();
  },
  render: function () {
    return <button onClick={this.handleLogOut}>Log Out</button>
  }
});

module.exports = LogOutButton;

If the component is dependent on another component (e.g. App is depedent on Header, then remember to require it at the top of the file, like so var Header = require('./Header.jsx');

Lastly, we need to get our App component from the components/App.jsx file. In CommonJS, we get other modules using require.


<script type="text/babel">
  var App = require('./components/App.jsx');
  ReactDOM.render(<App />, document.getElementById('renderTarget'));
</script>

This won't work, but let's try loading it up on the browser to see what happens anyways:

Uncaught ReferenceError: require is not defined

But why? This is because require is part of the CommonJS specification which is implemented only by server-side JavaScript environments, like NodeJS; it is not a standard in JavaScript itself, which means it is not defined in the browser.

And even if we did not use require, the module would pose a similar problem.

Uncaught ReferenceError: module is not defined

Hopefully, in a few years time, modules would be able to be loaded in the browser, using the new Loader standard. But until then, we'd have to find another solution.

So what's the solution? We can go back to using <script> tags, and making each component bind to the window or document global object. But making sure dependencies are loaded in the correct order would be a nightmare, and we won't be able to make use of the 350k+ CommonJS modules available on npm.

Of course, this problem was faced by millions of developers worldwide, and there have been many tools created to solve this issue.

We are going to explore two options - SystemJS and Webpack:

  • SystemJS solves the above issue by implementing the Loader specification in the browser. This allows you to load modules on the browser. SystemJS is a module loader.
  • Webpack solves the issue by resolving dependencies, downloading and bundling those dependencies along with the app, into one (or a few) large files. These bundled files contains the application and all its dependencies, and can be quite large. They then get sent over to the client, which will load it just like any other script.

The purpose of this series is to expose readers to the most common tools used when developing a React application; and as such, we will be using Webpack going forward, since it is, by far, the most popular method. We will, however, quickly go through setting it up with SystemJS, so those that are interested can get a taste of what it is about.

SystemJS

If the Loader specification is to come to fruition, we'll be able to load modules in the browser (as we tried to do). SystemJS is a library which attempts to mimic that behaviour.

SystemJS is a universal dynamic module loader; by 'universal', it means it can load not only CommonJS modules, but also ES6 modules, AMD and global scripts. We will demonstrate this later.

Let's include the SystemJS library by appending a <script> tag to the list of <script> tags. Remove the <script> tags for the components as SystemJS now handles loading the dependencies.


  <script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.41/system.js"></script>
</head>
  <script type="text/babel" src="/components/LogOutButton.jsx"></script>
  <script type="text/babel" src="/components/RegistrationForm.jsx"></script>
  <script type="text/babel" src="/components/LogInForm.jsx"></script>
  <script type="text/babel" src="/components/Header.jsx"></script>
  <script type="text/babel" src="/components/App.jsx"></script>

We do not need to do anything with our components modules, as they are already in CommonJS format.

Next, we need to use SystemJS.import to import our App module. SystemJS.import is akin to require, but because SystemJS also works with other module systems, such as ES6 and AMD, SystemJS.import is sort of a universal require that works for all modules.

SystemJS.import returns a promise, containing the element which was being imported.

This is similar to require.js, which worked for AMD modules.


var App = SystemJS.import('./components/App.jsx').then(App => {
  ReactDOM.render(<App />, document.getElementById('renderTarget'));
});

Let's go to localhost:8000 and see what's changed.

Still an error, but it is telling us that it doesn't recognize the JSX syntax. This is good news, because it has actually reached inside App.jsx, which means SystemJS.import is working!

So why isn't our type="text/babel" attribute working? Well, it is still working, otherwise we won't be able to use <App /> inside ReactDOM.render. So why is the code inside App.jsx throwing an error?

This is because SystemJS doesn't know that our .jsx file contains JSX (it's job is just to load the file), and because it's an external file (it is not in index.html, the Babel transpiler doesn't process it.

SystemJS + Babel

To resolve this, we'd simply need to tell SystemJS to use Babel to transpile the file first. To do this, we'd need to use SystemJS's plugins, specifically the Babel plugin. Babel has configured several preset configurations for common configurations, so we will tell the Babel loader plugin to use the babel-preset-react.

So, how do we add these plugins and npm packages? We can, as before, download the libraries and add them as script tags. However, those libraries have their own sets of dependencies, which must be downloaded also. Needless to say, resolving, downloading and ordering these dependencies manually is impractical, so we must use a package manager.

We could use npm or yarn to download our packages, but remember that SystemJS is meant to work with any module definition specifications, and even a repository URL. This means when SystemJS encounters a require('module_name') or an import module_name, it doesn't know where to find the module called module_name. Would it be on npm? Or is it a custom repository? SystemJS can't know for sure.

So we'd need to provide a mapping of all packages' names and the location of their repository. Needless to say, doing this manually is impractical, so we must use another package manager, that manages packages from everywhere.

Luckily for us, there is jspm, which stands for JavaScript Package Manager.

jspm

jspm is similar to npm and yarn, but it can download modules/packages from anywhere, not just from npm. Furthermore, it will automatically create a SystemJS configuration file with all the package-to-location mapping we talked about above.

First, download jspm as a global package.

$ npm install -g jspm # using npm
$ yarn global add jspm # using yarn

Then, just to make sure we use a consistent version of jspm, we should download jspm locally as well.

To use our React plugin, we'd need to use at least jspm v0.17, so we need to specify the version explicitly.

$ npm install jspm@0.17.0-beta.32 --save-dev
 # using npm
$ yarn add jspm@0.17.0-beta.32 --dev # using yarn

Then, in the project's root directory, run jspm init, and simply press the return key for each entry, except these:

  • Local package name (recommended, optional) to grapevine
  • SystemJS.config Node local project path [src/] to .
  • SystemJS.config transpiler (Babel, Traceur, TypeScript, None) [babel] - make sure this is babel

This will create a jspm.config.js, which houses the SystemJS configurations we need to load and transpile our modules, as well as a mapping of module names to the source of the module.

Next we need to install those plugin and that preset we mentioned earlier.

$ jspm install plugin-babel npm:babel-preset-react

Here, we're telling jspm to get the plugin-babel package, as well as the babel-preset-react package from the npm depository.

Wait a minute! Why does plugin-babel not need to be prepended with npm:? This is because jspm has a registry, where it maps the location of common packages to simpler names. If we search for the entry for plugin-babel, we'll see it's actually the same as npm:systemjs-plugin-babel.

This registry is an agreed set of mappings, we will have some mappings specific to our project as well, stored inside our jspm.config.js.

Going through the configuration file would take a long time, and is not the focus of this tutorial series. In short, these are the key properties and what they do:

  • paths - specify where to find a file given a certain prefix
  • transpiler - which transpiler to use
  • map - a mapping of package/module names to their location (as resolved according to the path property)
  • packages - a list of all packages our application uses

For now, we'll skip through the details of this configuration file and simply include it into our index.html.


  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.41/system.js"></script>
  <script src="./jspm.config.js"></script>
</head>

If we go to our site now, it still throws the same error saying it is not recognizing the JSX syntax. This is because we need to tell SystemJS to use the plugin-babel for our .jsx files.


<script type="text/babel">
  SystemJS.config({
    meta: {
      '*.jsx': {
        'loader': 'plugin-babel',
        'format': 'cjs',
        'babelOptions': {
          'modularRuntime': false,
          stage1: true,
          presets: ['babel-preset-react']
        }
      }
    },
  });
  var App = SystemJS.import('./components/App.jsx').then(App => {
    ReactDOM.render(<App />, document.getElementById('renderTarget'));
  });
</script>

All that is saying is - Find all files with the extension .jsx, transpile it using babel, with the preset stage1 and react, and expect the modules to be defined in CommonJS (cjs) format.

Now, if we reload our application, it should work with no problems!

As a side note, you can try using ES6 module syntax instead of CommonJS syntax, and it would still work because SystemJS is a universal module loader. For example, in the Header.jsx, try replacing:

var LogOutButton = require('./LogOutButton.jsx');

with:

import LogOutButton from './LogOutButton.jsx';

And it will work all the same!

Finally, get the complete source code for this article on GitHub!

Daniel Li

Full-stack Web Developer in Hong Kong. Founder of Brew.

Hong Kong http://danyll.com