Build a React Application from First Principles - ECMAScript 2015

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

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

So far, we've:

  • Created visual components for our application using React
  • Implemented authentication features by integrating with Firebase
  • Made the application code more modular using Webpack

All this time, we've been using ES5 syntax because it works everywhere (IE9+, and basically every other browser). However, ECMAScript 2015 (or more commonly known as ES6) is the new standard, and its most important features have already been supported by all major browsers. So in this article, we'll transform our code to ECMASCript 2015 syntax.

However, there are still a large number of browsers which do not support ECMAScript 2015, so when we create a bundle, we'd need to transpile our ECMASCript 2015 to ES5 syntax. Luckily for us, the Babel loader we used in the previous step does this automatically, so there's nothing extra we need to do.

This article would take one feature of ECMAScript 2015 at a time. If you ever get stuck, download the source code and reassure yourself.

ECMAScript Modules

As we've already mentioned, there are many module definition specifications - CommonJS, AMD, UMD. The ECMAScript 2015 specification tried to address this by introducing its own standard, in the hope of unifying them all.

It uses two keywords - import and export.

export

With CommonJS, we bind the thing we want to export, be it a string, a function, or object, to module.exports. With ECMAScript modules, we simply use export.

There are two types of exports - named and default. You can only have one default export per module, but many named exports.

// Default Exports
module.exports = App; // CommonJS  
export default App; // ECMAScript modules

// Named Exports
module.exports = {foo:foo, bar:bar }; // CommonJS  
export { foo, bar }; // ECMAScript modules

// Both Default and Named Exports
export { App as default, foo, bar };  

Go through each file now and change all module.exports statements to use export default instead.

It might be easier to use the global search functionality in your code editor to find all instances of module.exports.

import

With CommonJS, we use the require function to include an external module. In ECMAScript 2015, this is replaced by the keyword import and from.

var React = require('react'); // CommonJS  
import React from 'react'; // Importing default export from external ECMAScript module  
import { foo, bar } from '../another-module.js'; // Importing named exports from external ECMAScript module  

Now, replace all require statements with import and from.

ECMAScript 2015 in webpack.config.js

Now that we've updated our code with import and export, let's try bundling them again.

$ ./node_modules/.bin/webpack
/home/brew/projects/grapevine/webpack.config.js:1
(function (exports, require, module, __filename, __dirname) { import webpack from 'webpack';
                                                              ^^^^^^
SyntaxError: Unexpected token import  
...

It returns with an error to say import is not valid syntax in the webpack.config.js file. This is because whilst we configured Webpack to load our source files using Babel loader, nothing is there to tell Webpack to transform the actual webpack.config.js file itself. Luckily, all we have to do is make Babel recognize the file ES6 is to change its extension to .babel.js.

$ mv webpack.config.js webpack.config.babel.js

At the moment, our Babel configuration is on line 19 of our webpack.config.babel.js file, but the import statement is at the top; this means Babel never gets to read our configuration before it tries to parse it.

Therefore, we need to add some extra Babel configuration in a stand-alone .babelrc file.

.babelrc

{
  "presets": ["es2015"]
}

This .babelrc's presets does not require a React entry, as the webpack.config.babel.js does not use any JSX.

Finally, Webpack 2 brings native support ES6 Module, which means ES6 modules doesn't need to be transpiled to CommonJS first. To stop this unnecessary transpilation step, we should add an option to the es2015 preset.

Previously, we had to use babel-preset-es2015-webpack, but Babel 6.13.0 supports the module option now, so we should use that instead.


use: [
  {
    loader: "babel-loader",
    options: {
      "presets": [["es2015", {"modules": false}], "react"]
    }
  }
]

Note the nested array - es2015 and {"modules:false} is in the same array.

Now we're all set to try again!

$ ./node_modules/.bin/webpack
./dist/bundle.js  1.04 MB       0  [emitted]  [big]  app
   [0] ./~/process/browser.js 5.3 kB {0} [built]
   [2] ./~/fbjs/lib/warning.js 2.1 kB {0} [built]
  [15] ./~/react/lib/ReactElement.js 11.2 kB {0} [built]
  [17] ./~/react-dom/index.js 59 bytes {0} [built]
  [18] ./~/react/react.js 56 bytes {0} [built]
  [21] ./~/react/lib/React.js 2.69 kB {0} [built]
  [22] ./~/firebase/firebase-browser.js 259 bytes {0} [built]
  [24] ./~/firebase/app.js 16.4 kB {0} [built]
 [103] ./~/firebase/auth.js 119 kB {0} [built]
 [104] ./~/firebase/database.js 121 kB {0} [built]
 [105] ./~/firebase/messaging.js 16.9 kB {0} [built]
 [106] ./~/firebase/storage.js 29 kB {0} [built]
 [120] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
 [177] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
 [189] ./src/index.jsx 885 bytes {0} [built]
    + 175 hidden modules

const and let

When we declare a variable in ES5 and below, we use var, which is function-scoped; this often leads to unexpected errors (through poor use rather than a flaw in the language). For example, you can declare the same variable multiple times, using the variable inside a callback inside a loop can yield unexpected results,

ECMAScript 2015 introduces two new variable declaration keywords - const and let - which are block scope. In JavaScript, a block is a section of code enclosed in braces ({}).

const stands for constant, and it means the variable cannot be re-declared again. let is similar to var, but is block scope instead of function scope. The best practice is to use const as much as possible, and only use let when necessary.

Not that const does not mean the variable is immutable. When you declare an object with const, you are storing the reference to that object in the variable, not the object itself. This means the content of the object can still change, and thus not immutable.

With the introduction of const and let, there's no need to use var any longer. So go ahead and replace var with const.

Variable scope is a huge topic all on its own, and won't be covered in detail here. We do, however, encourage you to read more on it, as it is one of the most important aspect of JavaScript.

Arrow Functions

If you look at the handleSubmit method of LogInForm and RegistrationForm, you'll find this mysterious line:

const self = this;

This line exists to capture the value of this into a variable, so we can use it in the callback (when using promises) later. If we wrote this.setState inside the callback, the this would refer to the function, and not to the class, and thus it would throw an error saying there's no method called setState.

However, this is rather inelegant. So ECMAScript 2016 introduced the arrow function notation, which automatically binds the value of this and pass it into the function body.

This might sound a bit abstract to you, so let's see how our handleSubmit method would look like with arrow functions.


handleSubmit: function (e) {
  e.preventDefault();
  const self = this;
  firebase.auth().createUserWithEmailAndPassword(this.email.value, this.password.value)
    .then(function(user) => {
      console.log('User created successfully!');
      console.log(user);
    })
    .catch(function(error) => {
      console.log('Sign Up failed. See details below:');
      console.log(error);
      self this.setState({ // this refers to the React component class, and not the callback function
        errorMessage: error.message,
      });
    });
},

Instead of using function () {}, we instead write () => {}.

function (c) { this.password = c }  

becomes

(c) => { this.password = c }

As an exercise, turn all functions with .bind(this) to use arrow functions instead. Check out the complete code for hints if you get stuck.

If the arrow function only has one argument, the brackets may be omitted. E.g. (user) => {} is equivalent to user => {}. However, for clarity's sake, we'll leave it in.

Classes

ECMAScript added some syntactic sugar to how you can define 'classes' in JavaScript.

JavaScript doesn't really have classes, it has function constructors. But this syntactic sugar removes the complexity / technicality away, so you can use classes like you'd do in other languages with a object-oriented inheritance model.

If you want to know more about prototypical inheritance and function constructors, read this article on Understanding Prototypes in JavaScript

There's a lot of changes we need to make in order ot use ES6 syntax with React.

Class declaration

First, remove the createClass function, and instead use class and extends.

const App = React.createClass({  

becomes

class App extends React.Component {  
Class methods and constructor

Next, instead of getInitialState, we now set the state directly inside a constructor.

getInitialState: function () {  
  return {
    userIsLoggedIn: false
  };
},

becomes

constructor(props) {  
  super(props);
  this.state = {
    userIsLoggedIn: false
  }
}

Also note that it is not constructor = function () {}, but simply constructor(props). We are calling super here so the method body defined by its 'parent' class (React.Component) is ran before this child class.

Remember to change all other class methods to have the same syntax.

componentWillMount: function () {  

becomes

componentWillMount() {  

The Header element is a functional component, so we can leave that as it is.

Autobinding

With React.createClass, all the properties of the object passed into the function are autobounded to the class, which means we could write this.handleSubmit and the this inside the handleSubmit method would be the component class.

With ECMAScript class syntax, you'd need to do this binding manually. The simplest way it to append the method with .bind(this). So

<form onSubmit={this.handleSubmit}>  

becomes

<form onSubmit={this.handleSubmit.bind(this)}>  

There are other ways to achieve the same thing:

  • Bind the method in the constructor - this.handleSubmit = this.handleSubmit.bind(this);
  • Use lodash's bindAll in the constructor
  • Use an autobind decorator

But let's not digress and use .bind(this) for now.

Summary

In this article, we've transformed our code to use ECMAScript 2015 syntax.

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