React components with ES6

Having recently met Nicola at a conference I got immediately infected by his enthusiasm about React. It took me a few weeks to materialise things and get down to writing my first React component. Before we get started if you don't already know React advocates component based programming - simply put all what this means is that developers are encouraged to write small(er), reusable, loosely coupled components thereby adhering to the principle of the 'separation of concern'.

As I was completely new to React my first starting point was to look at their documentation and there I found a tutorial that walks you through on creating a component. It is a good starting point but I wanted to take this to the next level. Earlier back in January it was announced that React will support ES6 features such as classes - therefore the challenge was given. I want to create the component from the tutorial using ES6.

Let's talk about the setup first. ES6 is great and after much time reading about it and listening various presentations on it I have decided to roll up my sleeves and give it a go. This has resulted in a great application for a project I was working on and it also served as a good practice to get up and running with ES6. My preferred ES6 to ES5 compiler is Babel which I have previously installed on my system using npm install -g babel. I also have an src/es6 folder to store my app.es6 file - this is going to be where I'll have the source for my first ever component. To compile ES5 code I ran the following command: babel src/es6/app.es6 --out-file src/app.js. Once I have my app.js file I can easily run React's jsx on it by running the following command: jsx src/ build/.

Now, about the actual conversion from the ES5 code seen in the tutorial to ES6. There are a few things to remember, first, let's start with the most simple one - using ES6 classes.

The following code - which defines a React component can be very easily transformed into ES6:

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
React.render(
  <CommentBox />,
  document.getElementById('content')
);
class CommentBox extends React.Component {
  render() {
    return <div className="commentBox">
    Hello, world! I am a CommentBox.
    </div>
  }
}
React.render(<CommentBox />, content);

So far so good. At some stage in the tutorial a getInitialState method is added to the CommentBox - something along the lines of (this method is invoked once before the component is mounted and it's returned value will be passed to this.state - and as we are making a component to list comments we need to initialise this.state with an empty array.)

var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    // more code here
  }
});

Now let's have a look at this code using ES6. If you read the documentation thoroughly you'll notice that there's no need to specify this method when using classes but instead we can make use of the class constructor():

class CommentBox extends React.Component {
  constructor() {
    this.state = { data: [] }
  }
  
  render() {
    // more code here
  }
}

It is the constructor() where we setup the initial state for the data array (which is essentially an empty array).

Let's get onto the next interesting thing that caused me a bit of a headache. The tutorial displays the following code to retrieve the data from an external file (let's called this comments.json):

[
  {"author": "Tamas", "text": "Comment from Tamas"},
  {"author": "Mark", "text": "Comment from Mark"}
]
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

React.render(
  <CommentBox url="comments.json" pollInterval={2000} />,
  document.getElementById('content')
);

Looks straight forward. Let's go ahead and 'es6-ify' it. The final code is something similar to:

class CommentBox extends React.Component {
  constructor() {
    this.state = { data: [] }
  }

  loadCommentsFromServer() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: (data) => {
        this.setState({data: data});
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString());
      }
    });
  }

  componentDidMount() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  }

  render() {
    return <div className="commentBox">
    <h1>Comments
    <CommentList data={this.state.data} />
    </div>
  }
}

React.render(, content);

Running this code will load the comments fine however after 2 seconds (which is the pollInterval) you'll see the following error in the query console:

Curious isn't it? Let's do some debugging and add a console.log(this); statement to the loadCommentsFromServer() function just before the AJAX call. The result is interesting:

The first this shows information on CommentBox while all additional log statements print out information about the Window object (which is the JavaScript global in the browser) - so no wonder the URL property is undefined. How to fix this? We need to make sure that the loadCommentsFromServer stays inside the right scope. All we need to do is update the call for the function (notice the .bind(this) method):

componentDidMount() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer.bind(this), this.props.pollInterval);
  }

And this fixes the issue for good. For your reference here's the full ES6 version of the component:

class CommentList extends React.Component {
  render() {
    var commentNodes = this.props.data.map((comment) => {
      return (<Comment author={comment.author}>
      {comment.text}
      </Comment>)
    });
    return (<div className="commentList">
    {commentNodes}
    </div>)
  }
}

class CommentForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    var author = React.findDOMNode(this.refs.author).value.trim();
    var text = React.findDOMNode(this.refs.text).value.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    React.findDOMNode(this.refs.author).value = '';
    React.findDOMNode(this.refs.text).value = '';
    return;
  }

  render() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
      <input type="text" placeholder="Your name" ref="author" />
      <input type="text" placeholder="Your comment" ref="text" />
      <input type="submit" value="Post" />
      </form>
    )
  }
}

class CommentBox extends React.Component {
  constructor() {
    this.state = { data: [] }
  }

  loadCommentsFromServer() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: (data) => {
        this.setState({data: data});
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString());
      }
    });
  }

  handleCommentSubmit(comment) {
    var comments = this.state.data;
    var newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: (data) => {
        this.setState({data: data});
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString());
      }
    });
  }

  componentDidMount() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer.bind(this), this.props.pollInterval);
  }

  render() {
    return <div className="commentBox">
    <h1>Comments
    <CommentList data={this.state.data} />
    <CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)}/>
    </div>
  }
}

class Comment extends React.Component {
  render() {
    return <div className="comment">
    <h2 className="commentAuthor">{this.props.author}
    {this.props.children}
    </div>
  }
}

React.render(, content);

In order to run this example there are two more simple steps that you need to do. The first is to install babel by executing npm install babel and npm install react-tools.

First you need to convert the ES6 code to ES5 code by running babel src/to/app.es6 --out-file src/app.js and run it through the React JSX tool: jsx src/ build/. The build folder will then contain your .js file that you can include in your HTML file:

<div id="content"></div>
<script src="build/app.js"></script>

React is certainly an exciting library and I enjoyed working with it - I hope to roll up my sleeves again and put together some more exciting components, maybe using some more ES6.

Show Comments