Using ReactJS with Babel and ES6

I wanted to create this post shortly after the last post about Setting up ES6+Babel+Gulp, but it got delayed for some reasons. Anyways ..

Though it is not very complicated to setup ReactJS to work with Babel, there are a few non-obvious things you need to be aware of, specifically the absence of  auto-binding in ReactJS when coded in ES6. Let’s create a small NodeJS project using Gulp –

Install following packages –

npm install gulp gulp-connect vinyl-source-stream browserify babelify babel-preset-es2015 react react-dom babel-preset-react --save-dev

In addition to packages installed in the last post, here we are installing react, react-dom and babel-preset-react packages.

The directory structure is similar to one in the previous post, except that we add views folder for JSX files.

ES6ReactJSBabelTest
  -- build
  -- src
    -- js
       -- views
    -- html
  -- gulpfile.js

Gulp ‘build’ task is modified to add ‘react’ preset to babelify and add jsx extension to browserify (we want browserify to process .jsx files in addition to .js files)

gulpfile.js (partial)

gulp.task("build", function(){
    return browserify({
        entries: ["./src/js/index.js"],
        extensions: [".js", ".jsx"]
    })
    .transform(babelify.configure({
        presets : ["es2015", "react"]
    }))
    .bundle()
    .pipe(source("bundle.js"))
    .pipe(gulp.dest("./build"))
    ;
});

We will then create index.html in html folder with following content.

html/index.html

<!DOCTYLE html>

<html>
    <head>
        <title>Babel + ReactJS Test</title>
    </head>
    <body>
        <div id="main"/>
        <script src="bundle.js"></script>
    </body>
</html>

We will embed a ReactJS component in the main div above. Now we will create index.js in js folder which will create a ReactJS component and add it to the main div.

js/index.js

import React from "react";
import ReactDOM from "react-dom";
import MainView from "./views/MainView"

var mainView = React.createElement(MainView,{name:"Stranger"}, null);
ReactDOM.render(mainView, document.getElementById("main"));

Note that MainView is passed name attribute with “Stranger” value. Let’s now create the ReactJS Component, MainView in views folder.

views/MainView.jsx

import React from "react";

export default class MainView extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            name : props.name
        }
    }

    onSetName() {
        this.setState({
            name : this.refs.nameTxt.value
        })
    }

    render() {
        return <div>
            Hello {this.state.name} !
            <br/>
            Enter Name : <input type="text" ref="nameTxt" />
            <button type="button" onClick={this.onSetName}>Set Name</button>
        </div>
    }
}

Note that when you write ReactJS component in ES6, you do not override setState method, – you create this.state object in the constructor, if you want to initialize the component state. In the above example, we add name to state and initialize it with the name attribute/property passed to the component. Recall that in the index.js, we have passed name=”Stranger” to the MainView component.

In the render method we create a greeting string, using name stored in the state and also create a text box and a button to set name. onClick handler of the button is set to onSetName method of the component. So if you type something in the text box and click Set Name button then you would expect greeting to change with the content of the text box. However if you run this example (run gulp and browse to http://localhost:9001/index.html) and try to set name, you will see that greeting is not updated with the name. In the JS console you would see following error :

Uncaught TypeError: Cannot read property ‘setState’ of null

The reason is that ReactJS component is compiled to a new JS object with method names as keys and method body as values – here the code generated for our MainView component :

var MainView = function (_React$Component) {
    _inherits(MainView, _React$Component);

    function MainView(props) {
        _classCallCheck(this, MainView);

        var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(MainView).call(this, props));

        _this.state = {
            name: props.name
        };
        return _this;
    }

    _createClass(MainView, [{
        key: "onSetName",
        value: function onSetName() {
            this.setState({
                name: this.refs.nameTxt.value
            });
        }
    }, {
        key: "render",
        value: function render() {
            return _react2.default.createElement(
                "div",
                null,
                "Hello ",
                this.state.name,
                " !",
                _react2.default.createElement("br", null),
                "Enter Name : ",
                _react2.default.createElement("input", { type: "text", ref: "nameTxt" }),
                _react2.default.createElement(
                    "button",
                    { type: "button", onClick: this.onSetName },
                    "Set Name"
                )
            );
        }
    }]);

    return MainView;
}(_react2.default.Component);

As you can see, setName method becomes value of a field setName in the object passed to _createClass method. ‘this’ scope in this event handler method is n0t the same as ‘this’ scope for the component. If you write ReactJS code in ES5, then the compiler does auto-binding of user defined component methods to the component, but it does not do so when the code is written in ES6. So we need to manually do this auto-binding. One way to do this is to specifically bind user defined methods to the component in the constructor –

constructor (props) {
        super(props);
        this.state = {
            name : props.name
        }
        this.onSetName = this.onSetName.bind(this);
    }

But this is not a very good solution, because you will have to do this for all user defined functions in your component.

Other solution is to use fat arrow, =>, functions of ES6. When you declare function using fat arrow, the function does not create its own ‘this’ scope, but uses the active ‘this’ scope at that point. So we could write setName event handler in our component as –

onSetName = () => {
        this.setState({
            name : this.refs.nameTxt.value
        })
    }

But this code won’t compile in ES6 (try gulp command to build the application), because ES6 does not support property assignment in classes. This is supported in ES7. The good news is that there is a Babel preset (Stage-0 preset) that can translate this code to ES5. So install the preset

npm install babel-preset-stage-0 --save-dev

You also need to add this preset to babelify in the build task to gulpfile.js

.transform(babelify.configure({
        presets : ["es2015", "react", "stage-0"]
    }))

Remember to remove binding code we added in the constructor, if you are going to use the above Babel preset.

With stage-0 preset added, our code should compile and run as expected.

-Ram Kulkarni

Leave a Reply