Migrating a React app to ES6

October 16, 2016

In 2015, the ECMAScript language specification which is used as the standard for JavaScript in many browsers published its 6th version. This update is the successor to the version published in 2009 and a lot has changed in the JavaScript world since then.

Moving to ES6

One of our React apps at work was being updated to a new linter configuration to conform with the Airbnb ES6 standard and it gave me an opportunity to learn about many of the new feature and syntax improvements that have been introduced in this update.

I’m only going to go over a few of the most significant and interesting changes that we have had to make in our updated applications but the entire specification is available here.

Arrow Functions

ES6 introduced a new function shorthand syntax and it has quickly become one of my favorite new features of the language. Arrow functions replace the classic function() {}, making code much less verbose.

items.map(function(item) {
  return item.name;
});

// Arrow syntax
items.map(item => {
  return item.name;
});

// Single line functions omit a return and curly braces
items.map(item => item.name);

In addition to cleaning up the function syntax, the power of the arrow function comes from its implicit binding of the function’s this keyword to its value in the enclosing scope. In the following example, a React class needs to render the HTML for each item in its passed down properties:

var GroceryList = React.createClass({
  getDefaultProps: function() {
    return {
      items: [{ id: 1, name: 'Bread' }]
    };
  },
  renderItem: function(name) {
    return <div>{name}</div>;
  },
  render: function() {
    var items = this.props.items.map(function(item) {
      return this.renderItem(item.name);
    }.bind(this));
    return (
      <div>{items}</div>
    )
  }
})

The items mapping has to bind this in order for the call-site of renderItem to have to the function. With arrow functions, the mapping can be expressed more simply as shown below:

var items = this.props.items.map((item) => (
  this.renderItem(item.name);
));

Other use cases for arrow functions include callbacks to asynchronous functions such as with AJAX requests or timeouts.

Classes

One of the most interesting changes in ES6 is the introduction of classes. Object-oriented classes were previously achievable in JS using prototype-based OO patterns, but the introduction of the official class syntax provides a consolidated, clean class interface. React has embraced the use of ES6 classes and now encourages the use of component extension over the traditional React.createClass.

The example below illustrates the new class-based React component syntax:

import React, { Component } from React;
class ContainerItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      name: `Container Item for ${props.container}`
    };
  }
  render() {
    const { name } = this.state;
    return <div>{name}</div>
  }
};
ContainerItem.defaultProps = {
  container: null
};

At this point you may have some questions such as the following:

Where did getInitialState go? What does super do? Wait why are there backticks and what is even happening with that first line in the render method?

All in good time. The above ES6 class is a lot to process when coming from an ES5 world so let’s break it down into parts:

  1. The extends keyword is used to create a class that inherits from the extended class.

  2. On instantiation, the constructor method is called automatically and it can call the extended class’s constructor using the super keyword. In React, the parent class must call super in order for this to be available in the constructor.

  3. The initial state for the React class is set in the constructor, replacing the getInitialState method. The getDefaultProps method has been similarly replaced by the defaultProps property.

  4. Backticks are used to support ES6 template strings, which are similar to the string interpolation mechanisms in other languages like Python or Ruby. Gone are the days of concatenating strings using + and it is a welcome change.

  5. The render method is using two small but important new features from ES6. The first is the const keyword for variables and must be initialized with a value. If you try and change the value of a const variable after its declaration it will immediately throw a TypeError. The mutable version of this keyword is let and both the new variable types are block-scoped in order to address unwanted variable hoisting and scope congestion such as in the situation below:

function a() {
	console.log(x); // Undefined
	console.log(y); // Throws a ReferenceError
	if (true) {
		var x = 2;
	}
}
a();

The programmer’s intention is often to use variable x only in the context of the conditional statement, however, variable hoisting declares x in the scope of the function, making it undefined when it is printed rather than throwing a ReferenceError like y.

Replacing var with let or const would cause x to also throw a reference error and helps the developer determine what went wrong.

The second feature introduced in the render method of our ES6 React component is called destructuring. Destructuring uses pattern matching and in the simplest case like the one above, it creates a const local variable called name equal to the value of the name property from the state object. Naming local variables differently from the properties of the object can similarly be achieved in our example using more complex pattern matching: const { newName: name } = this.state. Destructuring can be nested arbitrarily and is a powerful new feature of the language.

Modules

The first line of our new React component imported React and Component from the React library using new language-level support for modules in JavaScript with influences from the popular CommonJS and AMD formats. Building imports into the language provides developers with a consistent and optimized module syntax that can be statically analyzed.

The following example shows the basic use of this import/export functionality:

//------ lib.js ------
export const pi = 3.14159;
export function circumference(radius) {
    return 2 * pi * radius;
}
export function area(radius) {
    return pi * radius * radius;
}

//------ main.js ------
import { circumference, area } from 'lib';
console.log(circumference(5));
console.log(area(5));

If we wanted to import all of the exports from the lib file we could use the syntax import * as lib from 'lib' to name the aggregate exports under the lib object.

In the case that we want our module to consist of only a single export, then an ES6 module can use the default keyword to select a default export that can then be implicitly imported from other files:

//------ Example.js ------
export default class { ... };

//------ import.js ------
import Example from 'Example';

Thoughts

After having migrated all of our components to the new ES6 syntax and used the new language features on a daily basis for the past month I have found the ES6 specification to be a welcome addition to JavaScript. Our conversion has resulted in clean, readable code that takes advantage of the powerful new features of ES6 and I’m looking forward to seeing how the language continues to evolve. For anyone who is interested, you can read up on the already drafted ECMAScript 2017 standard here.