Matt Kane

Class fields are coming: here's what that means for React

January 04, 2019

If you’ve ever written a class component in React, you probably have a contructor a lot like this:

import React, { Component } from "react";

export class Incrementor extends Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    };
    this.increment = this.increment.bind(this);
  }

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <button onClick={this.increment}>Increment: {this.state.count}</button>
    );
  }
}

This is the pattern used throughout the React docs, and seems to be the most common approach that I’ve seen in the wild. If you’re anything like me, you forget to bind the event handler until you get the ubiquitous this is undefined; can't access its "setState" property error.

TypeScript users are probably looking at this and wondering why this song and dance is needed. The most idiomatic TypeScript way is probably this:

import * as React from "react";

interface State {
  count: number;
}

export class Incrementor extends React.Component<{}, State> {
  state = { count: 0 };

  increment = () => this.setState({ count: this.state.count + 1 });

  render() {
    return (
      <button onClick={this.increment}>Increment: {this.state.count}</button>
    );
  }
}

Instead of initialising state in the constructor, it is a property of the class. The increment method has been changed to an arrow function. This means that there is no need to bind it: it already has access to this from the component. We can actually change the render() method to an arrow function too. This doesn’t gain us anything in terms of scope, but to me looks a lot clearer:

import * as React from "react";

interface State {
  count: number;
}

export class Incrementor extends React.Component<{}, State> {
  state = { count: 0 };

  increment = () => this.setState({ count: this.state.count + 1 });

  render = () => (
    <button onClick={this.increment}>Increment: {this.state.count}</button>
  );
}

Try it

Now, a lot of people reading this are probably thinking “well, duh: we’ve been able to do this JavaScript for ages”, which is true, but only if you have the right Babel plugin. This isn’t standard ECMAScript, and isn’t enabled by default. However it is used by default in create-react-app, so quite a few people thought it was standard. I know I did. If you have the plugin enabled, you can write the following, which is virtually identical to the TypeScript:

import React from "react";

export class Incrementor extends React.Component {
  state = { count: 0 };

  increment = () => this.setState({ count: this.state.count + 1 });

  render = () => (
    <button onClick={this.increment}>Increment: {this.state.count}</button>
  );
}

Try it

Much neater, right?

Class field functions aren’t the solution to everything though. For a start, you can’t override them in subclasses, nor use them to override superclass methods. For that reason you can’t use them for lifecycle methods. There are also potential performance issues if you are creating loads of copies of a component. While a class method is created once on the prototype, class fields are created on each object: each component will have its own copy of each function. However this is only likely to be an issue if you are creating hundreds of instances.

Why now?

The class-properties plugin has been available for a couple of years now, so why am I writing this post now? Well, a couple of things have changed recently. The class fields proposal has been wending its way through the TC39 ECMAScript process for years, and is now at Stage 3, which is the final stage before approval. It has been quite a contentious proposal though, and has been at Stage 3 since July 2017. That is largely due to disagreements about private field syntax and implementation. However it seems like the standardisation process is nearly there, and there has been an important development in the past month: browser support has landed. Chrome 72 (and V8 v7.2) will enable public fields by default, with private fields available behind a flag. This will be released on 29th Jan 2019. Meanwhile, support should be landing soon in Firefox, and Safari. The most recent TC39 update was that they’d aim to move to stage 4 (finished) once there are two implementations. That looks like it will be imminent.

Of course we all know that hooks are the way forward, but Facebook has made it clear that class components aren’t going anywhere. I would like to make a plea that now is the time to move to take the plunge and move to class fields. Banish the constructor (most of the time)!


I'm Matt Kane. I've made high-speed flashes and beekeeping software, but I mostly spend my time making web and mobile apps with React and TypeScript. Follow me on Twitter and Github.