Skip to main content

React components with ES6

5 min read

Older Article

This article was published 11 years ago. Some information may be outdated or no longer applicable.

I recently met Nicola at a conference and caught his enthusiasm about React immediately. It took a few weeks to sit down and write my first React component. Quick background if you’re new to React: it pushes component-based programming. Small, reusable, loosely coupled pieces that follow the separation of concern principle.

Since I was brand new to React, I started with their tutorial that walks through building a component. Good starting point, but I wanted to push further. Back in January, the team announced that React would support ES6 features like classes. Challenge accepted. I’d rebuild the tutorial component using ES6.

Let’s talk setup first. I’d spent time reading about ES6, listening to presentations, and finally rolled up my sleeves. My preferred ES6-to-ES5 compiler is Babel, installed via npm install -g babel. I keep an src/es6 folder for my app.es6 file. To compile: babel src/es6/app.es6 --out-file src/app.js. Then run React’s jsx on it: jsx src/ build/.

Now, the actual conversion from ES5 to ES6. A few things to remember, starting with the simplest: ES6 classes.

This code defines a React component and transforms easily 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. The tutorial later adds a getInitialState method to CommentBox. This method fires once before the component mounts, and its return value becomes this.state. Since we’re listing comments, we initialise with an empty array:

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

Here’s the ES6 version. If you read the documentation carefully, you’ll notice there’s no need for getInitialState with classes. We use the constructor() instead:

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

  render() {
    // more code here
  }
}

The constructor() sets up the initial state for the data array (an empty array, in this case).

Now for the part that gave me a headache. The tutorial shows this code for loading data from an external file (let’s call it 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 clear enough. Let’s convert it to ES6:

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);

Run this and the comments load fine. But after 2 seconds (the pollInterval), you’ll hit this error in the console:

Curious. Let’s drop a console.log(this); into loadCommentsFromServer() just before the AJAX call. The result tells the whole story:

The first this points to CommentBox. Every subsequent log statement prints the Window object (JavaScript’s global in the browser). No wonder the URL property is undefined. The fix? Keep loadCommentsFromServer bound to the right scope. Add .bind(this):

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

That sorts it out permanently. 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);

To run this, you’ll need two packages: npm install babel and npm install react-tools.

First, convert ES6 to ES5: babel src/to/app.es6 --out-file src/app.js. Then run it through React’s JSX tool: jsx src/ build/. The build folder will contain your .js file, ready to include in HTML:

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

React’s been a genuinely exciting library to work with. I’m looking forward to rolling up my sleeves again and building more components, hopefully with even more ES6.