Refactoring React Components to ES6 Classes

Here at NMC, we're big fans of the React library for building user interfaces in JavaScript. We've also been experimenting with the next version of JavaScript, ES6, and were excited to see the latest version of React promote ES6 functionality. Starting with React 0.13, defining components using ES6 classes is encouraged.

Refactoring a React 0.12 component defined using `createClass` to an 0.13 and beyond class only requires a few straightforward refactoring steps. In this blog post, we'll walk through them one-by-one.

Step 1 - Extract `propTypes` and `getDefaultTypes` to properties on the component constructor

Unlike object literals, which the `createClass` API expected, class definitions in ES6 only allow you to define methods and not properties. The committee's rationale for this was primarily to have a minimal starting point for classes which could be easily agreed upon and expanded in ES7. So for class properties, like `propTypes`, we must define them outside of the class definition.

Another change in React's 0.13 release is that `props` are required to be immutable. This being the case, `getDefaultProps` no longer makes sense as a function and should be refactored out to a property on the constructor, as well.

Before:

var ExampleComponent = React.createClass({
 propTypes: {
  aStringProp: React.PropTypes.string
 },
 getDefaultProps: function() {
  return { aStringProp: '' };
 }
});

After:

var ExampleComponent = React.createClass({ ... });
ExampleComponent.propTypes = {
 aStringProp: React.PropTypes.string
};
ExampleComponent.defaultProps = {
 aStringProp: ''
};

Step 2 - Convert component from using `createClass` to being an ES6 Class

ES6 class bodies are more terse than traditional object literals. Methods do not require a `function` keyword and no commas are needed to separate them. This refactoring looks as such:

Before:

var ExampleComponent = React.createClass({
 render: function() { 
  return <div onClick={this._handleClick}>Hello, world.</div>;
 },
 _handleClick: function() {
  console.log(this);
 }
});

After:

class ExampleComponent extends React.Component {
 render() { 
  return <div onClick={this._handleClick}>Hello, world.</div>;
 }
 _handleClick() {
  console.log(this);
 }
}

Step 3 - Bind instance methods / callbacks to the instance

One of the niceties provided by React's `createClass` functionality was that it automatically bound your methods to a component instance. For example, this meant that within a click callback `this` would be bound to the component. With the move to ES6 classes, we must handle this binding ourselves. The React team recommends prebinding in the constructor. This is a stopgap until ES7 allows property initializers.

Before:

class ExampleComponent extends React.Component {
 render() { 
  return <div onClick={this._handleClick}>Hello, world.</div>;
 }
 _handleClick() {
  console.log(this); // this is undefined
 }
}

After:

class ExampleComponent extends React.Component {
 constructor() {
  super();
  this. _handleClick = this. _handleClick.bind(this);
 }
 render() { 
  return <div onClick={this._handleClick}>Hello, world.</div>;
 }
 _handleClick() {
  console.log(this); // this is an ExampleComponent
 }
}

As a bonus step, at the end of this post we'll look at introducing our own Component superclass that tidies up this autobinding.

Step 4 - Move state initialization into the constructor

The React team decided a more idiomatic way of initializing state was simply to store it in an instance variable setup in the constructor. This means you can refactor away your `getInitialState` method by moving its return value to be assigned to the `this.state` instance variable in your class' constructor.

Before:

class ExampleComponent extends React.Component {
 getInitialState() {
  return Store.getState();
 }
 constructor() {
  super();
  this. _handleClick = this. _handleClick.bind(this);
 }
 // ...
}

After:

class ExampleComponent extends React.Component {
 constructor() {
  super();
  this. _handleClick = this. _handleClick.bind(this);
  this.state = Store.getState();
 }
 // ...
}

Conclusion

The handful of refactoring steps needed to convert an existing component to an ES6 class / React 0.13 and beyond component is pretty straightforward. While `React.createClass` is not deprecated, and will not be until JavaScript has a story for mixins, there is a strong consensus that working in the direction the language is heading is wise.

As a closing thought, consider one additional refactoring that introduces your project's own base Component class to hold niceties that are reused through your own Component library.

Bonus Step - Refactor to a base component

Before:

class ExampleComponent extends React.Component {
 constructor() {
  super();
  this. _handleClick = this. _handleClick.bind(this);
  this. _handleFoo = this. _handleFoo.bind(this);
 }
 // ...
}

After:

class BaseComponent extends React.Component {
 _bind(...methods) {
  methods.forEach( (method) => this[method] = this[method].bind(this) );
 }
}
 
class ExampleComponent extends BaseComponent {
 constructor() {
  super();
  this._bind('_handleClick', '_handleFoo');
 }
 // ...
}

Notice how we've reduced the tedium of binding multiple instance methods to `this` by writing a `_bind` helper method in our `BaseComponent`. The `_bind` method uses a couple of awesome ES6 features: `methods` is a rest parameter, and there's an arrow function in the `forEach`. If you're unfamiliar with these features of ES6, I'll leave them as cliffhangers for you to explore further. Happy trails.

Comments

shane davis's avatar
shane davis
Great article, just what I was looking for when faced with 'this' is undedifined! Thanks for taking the time to post this, saved me lots!!
rosdi's avatar
rosdi
Hi thanks for this.. I finally manage to convert mine to ES6 syntax!
ilia's avatar
ilia
I found this way more expressive: onClick={() => this.handleClick()}
myagoo's avatar
myagoo
Your After Step 2 example is missing explicit this binding

class ExampleComponent extends React.Component {
render() {
return Hello, world.;
}
_handleClick() {
console.log(this);
}
}
myagoo's avatar
myagoo
But that was before I read step 3.. Sorry ^^'
Sean's avatar
Sean
Great article, helped a bunch! One thing you may want to add is that this.getDOMNode is deprecated in ES6 based components too. All references to getDOMNode need to be changed to use the new React.findDOMNode.
Tony's avatar
Tony
Thanks for the article, helped a lot. I wonder why this. is followed by a space in a constructor method binding?
Alan Meira's avatar
Alan Meira
Hey man! Do you use React together with Slim? There are any article about it?
Mark's avatar
Mark
Out of curiosity, what do you do with mixins? Have you ever migrated an app that uses them extensively to es6?
Johan's avatar
Johan
But what are the benefits of moving to ES6 classes?

I agree the the explicit bind, which is done automagically with createClass is clearer/consistent but beyond that using classes has the side-effect of introducing a hierarchy into components. This makes the components brittle as a change in the base class will ripple though other components. Seems prototypal OO which is standard is a better solution. Eric Elliot (@_ericelliot) has written about this extensively.

Add to this that now you have to wait for ES7 to get back to functionality you already have the real benefits seem doubtful.
Kris Jordan's avatar
Kris Jordan NMC team member
@Mark - I have not, but there appear to be projects bridging this gap such as https://github.com/brigand/react-mixin

@Johan - The React team's official stance is moving in the grain of idiomatic JavaScript:

"JavaScript originally didn't have a built-in class system. Every popular framework built their own, and so did we. This means that you have a learn slightly different semantics for each framework.

We figured that we're not in the business of designing a class system. We just want to use whatever is the idiomatic JavaScript way of creating classes."

Source: https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html

The pros and cons of moving to ES6 classes would be a great blog post in itself. The short story is this defense: classes wind up being syntactic sugar on top of the prototypal JS system (that's why they can be transpiled to ES5). You can still hack on the prototype chain, if you want. Mixins tend to be syntactic sugar for what could otherwise be achieved with composition. Having a "language standard" way of defining classes (ES6) and mixins (future) is really beneficial in the long run, even if it comes at some cost in complete flexibility, because it allows the entire ecosystem to work together more effectively.
arturkin's avatar
arturkin
Hi, thanks for the article.
I'm facing a problem with your last code example with extending base component. It throws Uncaught TypeError: this.$ExampleComponent_bind is not a function.
Luca Colonnello's avatar
Luca Colonnello
Have you consider to use ES7 binding?
{::this.handleClick}
Fei's avatar
Fei
y, the same error with Arturkin. After I replace this to super. I got another error like below:

Uncaught TypeError: Cannot read property 'call' of undefined

Idea?
Thanks in advance.
Alex Schenkman's avatar
Alex Schenkman
Have you seen this post?
It covers many of your points with a nicer syntax.
Thank you!
Alexander's avatar
Alexander
Why do you introduce inheritance, when simple function will do the job?

function bind(obj, ...methods) {
methods.forEach( (method) => obj[method] = obj[method].bind(obj) );
}

And you it like this:

bind(this, "onClick", "onSubmit", "onEverythingElse");
Mike's avatar
Mike
Awesome blog post!
Dan Rostron's avatar
Dan Rostron
React doesn't allow more than one level of inheritance in components. So you're last example won't work. You can only extend React.Component.
Aaron Hardy's avatar
Aaron Hardy
How about just this:

class ExampleComponent extends React.Component {
_handleClick = event => {
console.log(this); // this is the component
};
...
}
Max Ho's avatar
Max Ho
Yeah, agree with Aaron. Just use arrow functions. Check this out - http://babeljs.io/blog/2015/06/07/react-on-es6-plus/

Thanks for the article!
tnRaro's avatar
tnRaro
How about this:

class ExampleComponent extends React.Component {
render(){
return Hello, world.;
}
_handleClick(e){
console.log(this, e);
}
}
Clint's avatar
Clint
I've found that declaring `propTypes` as a static method also works (vs. adding it outside of the class definition):

```
class MyComponent extends React.Component {
static propTypes() {
return {
location: React.PropTypes.string
};
}
}
```
Alex Liu's avatar
Alex Liu
Awesome! Thanks. How about this : console.log(this)}>
Bharat's avatar
Bharat
I need one clearity, In es6 what is the purpose of componentDidMount when I can use all the thing s in the constructor?
do we really need componentDidMount in es6?
Bharat's avatar
Bharat
there is typo error in my previous comment.. Please replace "componentDidMount" with "componentWillMount "
Adam's avatar
Adam
@Johan:

"This makes the components brittle as a change in the base class will ripple though other components."

That's exactly the definition of a "base class". I'm not exactly sure of your concern. The use case for a base class is pretty much "hey, I don't want to have to do this in a million places - so, I'll use a base class to these other classes so that 'a change in the base class will ripple through other components (classes)."

Or is there something more subtle you're saying that I'm missing?
Vyacheslav's avatar
Vyacheslav
Clint, maybe you forget make static propTypes as getter, e.g.
...
static get propTypes() {
 return {
  location: React.PropTypes.string
 };
}
...
or maybe I just use another version of Reactjs. Because only getter works for me.
Diego Gallegos's avatar
Diego Gallegos
Awesome post. I was fighting with es6 on Touchable HIghlight on react native. Read this post and bum! it works!
Anthony Lapenna's avatar
Anthony Lapenna
Cool post, thanks a lot for the bonus part (awesome) !
Joe's avatar
Joe NMC team member
Do you have a similar post for v15 onwards?
Gabriel's avatar
Gabriel
Thanks for this post!

Leave a comment

Real Time Web Analytics