Back to the future: ES6 + React

I've just recently finished shaving about a billion yaks * to convert a React app over to use ES6 modules and classes so we can start living in the future that is ES6 with a sprinkling of ES7.

* Might not be true

Transpiling back to the present

We're using babel via webpack to transpile our ES6+ code into ES5. Babel exposes the various stages of ECMAScript proposals so you can choose whatever stages are appropriate for your project. For example Stage 0 will give you the most features, but that will include things like strawman proposals that may never actually see the light of day.

For our project we've tried to stick to Stage2 which is Babel's default (Stage 2 features Draft specs), and then add to that a couple of more modern features which should hopefully make it to prime time.

The two features we've specifically added are:

ES6 Modules

Es6 Modules are very powerful, it takes a bit of getting used to coming from commonJS or AMD modules.

One of the really great things is that you can export more than one thing by using a default export and exporting something else.
In this example the instance will be the default export that you'll get when importing this module:

E.g.

export class Tracking {  
    // Whatever
}

export default new Tracking();  

So now I have two options:

// Get an instance
import tracking from tracking;  
tracking.init();

// Get the class (handy for tests)
import { Tracking } from tracking;  
var tracking = new Tracking();  
tracking.init();  

There's lots of great info on the full range or what's possible with es6 modules here.

React components with ES6 classes

When converting from ES5 there's quite a number of things to be aware of.

Setting default state

You can easily do this in the constructor rather than using a separate getInitialState method.

import React, { Component } from 'react';

class MyComponent  extends Component {  
    constructor() {
        super();
        this.state = {
            // Default state here.
        }
    }

    //... Everything else  
}

Setting defaultProps and propTypes

When using defaultProps and propTypes are defined as static class properties.

If you have es7.classProperties you can do this like so:

import React, { Component, PropTypes } from 'react';

class MyComponent  extends Component {

    static propTypes = {
        foo: PropTypes.func.isRequired,
        bar: PropTypes.object,
    }

    static defaultProps = {
        foo: () => {
               console.log('hai');
        },
        bar: {},
    }

    //... Everything else
}

The alternative to this (when you don't have es7 shiny enabled) is to just set props directly on the class e.g:

class MyComponent  extends Component {  
    //... Everything else
}
MyComponent.propTypes = {foo: PropTypes.func.isRequired};  
MyComponent.defaultProps = {foo: function() { console.log('hai'); }};  

I found this very ugly though, so static class props seems to be a good way to go for now.

isMounted() isn't available

If you'd used isMounted() then this isn't availabe on ES6 classes extended from Component. If you really need it you can set a flag in componentDidMount and unset it again in componentWillUnMount.

Method binding

When passing methods around as props you'll find with ES6 classes this doesn't refer to the component like it used to with React.createClass.

To get back the same behaviour without using something ugly like:

<Foo onClick={this.handleClick.bind(this)} />  

You can use an arrow function instead:

class MyComponent  extends Component {  
    handleClick = (e) => {
        // this is now the component instance.
        console.log(this);
    }
}

This works because arrow functions capture the this value of the enclosing context. In otherwords an arrow function gets the this value from where it was defined, rather than where it's used.

If you want to get really fancy you can use "function bind syntax".

Which you get by doing this:

<Foo onClick={::this.handleClick} />  

Which is equivalent to the previous bind() example. But to do that you'd need to enable es7.functionBind. For me this was a step too far and I'm happy to stick with the arrow functions.

Testing with ES6/7

Our previous tests had made some good use of rewire.js to modify requires and vars that weren't exported from the module (It does this with some cheeky injection techniques).

Unfortunately due to some changes in Babel rewire isn't compatible (at the moment) with ES6 modules. There's an alternative babel-plugin-reqire but that injects getters and setters into every module.

Dependency Injection in React Classes

Instead of using module mangling it was easiest to fall-back to dependency injection for the places where we were changing Components to fake ones.

In React props look like a good fit for dep injection:

import DefaultOtherComponent from 'components/other';

class MyComponent extends Component {

    static defaultProps: {
        OtherComponent: DefaultOtherComponent,
    }

    //... Snip

    render() {
        var OtherComponent = this.props.OtherComponent;
        return (
          <OtherComponent {...this.props} />
        );
    }
}

Now in the test we can inject a fake component:

TestUtils.renderIntoDocument(  
  <MyComponent OtherComponent={FakeOtherComponent} />
);

getDOMNode() is deprecated

In ES6 React classes getDOMNode() is not present so you'll need to refactor all calls to MyComponent.getDOMNode() to React.findDOMNode(MyComponent) instead. The docs also mention it's deprecated so it might be removed completely in the future.

Summary

There's quite a bit of work in making the conversion depending on how big your code-base is and depending on how much use of ES6 you're making already.

A lot of the new features in ES6 are making things much easier for developers so I'd definitely say it's a direction worth moving in assuming you're comfortable with all the aspects of transpilation. Webpack + babel makes this pretty straightforward. If you're already using babel for JSX (or some other similar loader for JSX) then you're already most of the way there.

FWIW: if you're not using babel, switching to it is now an official recommendation.

I'm very impressed with the currently ecosystem around JS e.g. tools like babel and webpack alongside forward thinking libs like React + Redux. All of these things sit well with each other and allow projects like ours to be able to step into the future today.

Now we've just got to get used to all the new syntax and start using more of it!

Further reading

Credits

  • Image By Terabass (Own work) CC BY-SA 4.0, via Wikimedia Commons
comments powered by Disqus