overreactedby Dan Abramov

Why Do We Write super(props)?

November 30, 2018

I heard Hooks are the new hotness. Ironically, I want to start this blog by describing fun facts about class components. How about that!

These gotchas are not important for using React productively. But you might find them amusing if you like to dig deeper into how things work.

Here’s the first one.


I wrote super(props) more times in my life than I’d like to know:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}

Of course, the class fields proposal lets us skip the ceremony:

class Checkbox extends React.Component {
  state = { isOn: true };
  // ...
}

A syntax like this was planned when React 0.13 added support for plain classes in 2015. Defining constructor and calling super(props) was always intended to be a temporary solution until class fields provide an ergonomic alternative.

But let’s get back to this example using only ES2015 features:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
  }
  // ...
}

Why do we call super? Can we not call it? If we have to call it, what happens if we don’t pass props? Are there any other arguments? Let’s find out.


In JavaScript, super refers to the parent class constructor. (In our example, it points to the React.Component implementation.)

Importantly, you can’t use this in a constructor until after you’ve called the parent constructor. JavaScript won’t let you:

class Checkbox extends React.Component {
  constructor(props) {
    // 🔴 Can’t use `this` yet
    super(props);
    // ✅ Now it’s okay though
    this.state = { isOn: true };
  }
  // ...
}

There’s a good reason for why JavaScript enforces that parent constructor runs before you touch this. Consider a class hierarchy:

class Person {
  constructor(name) {
    this.name = name;
  }
}
 
class PolitePerson extends Person {
  constructor(name) {
    this.greetColleagues(); // 🔴 This is disallowed, read below why
    super(name);
  }
  greetColleagues() {
    alert('Good morning folks!');
  }
}

Imagine using this before super call was allowed. A month later, we might change greetColleagues to include the person’s name in the message:

  greetColleagues() {
    alert('Good morning folks!');
    alert('My name is ' + this.name + ', nice to meet you!');
  }

But we forgot that this.greetColleagues() is called before the super() call had a chance to set up this.name. So this.name isn’t even defined yet! As you can see, code like this can be very difficult to think about.

To avoid such pitfalls, JavaScript enforces that if you want to use this in a constructor, you have to call super first. Let the parent do its thing! And this limitation applies to React components defined as classes too:

  constructor(props) {
    super(props);
    // ✅ Okay to use `this` now
    this.state = { isOn: true };
  }

This leaves us with another question: why pass props?


You might think that passing props down to super is necessary so that the base React.Component constructor can initialize this.props:

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

And that’s not far from truth — indeed, that’s what it does.

But somehow, even if you call super() without the props argument, you’ll still be able to access this.props in the render and other methods. (If you don’t believe me, try it yourself!)

How does that work? It turns out that React also assigns props on the instance right after calling your constructor:

  // Inside React
  const instance = new YourComponent(props);
  instance.props = props;

So even if you forget to pass props to super(), React would still set them right afterwards. There is a reason for that.

When React added support for classes, it didn’t just add support for ES6 classes alone. The goal was to support as wide range of class abstractions as possible. It was not clear how relatively successful would ClojureScript, CoffeeScript, ES6, Fable, Scala.js, TypeScript, or other solutions be for defining components. So React was intentionally unopinionated about whether calling super() is required — even though ES6 classes are.

So does this mean you can just write super() instead of super(props)?

Probably not because it’s still confusing. Sure, React would later assign this.props after your constructor has run. But this.props would still be undefined between the super call and the end of your constructor:

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}
 
// Inside your code
class Button extends React.Component {
  constructor(props) {
    super(); // 😬 We forgot to pass props
    console.log(props);      // ✅ {}
    console.log(this.props); // 😬 undefined 
  }
  // ...
}

It can be even more challenging to debug if this happens in some method that’s called from the constructor. And that’s why I recommend always passing down super(props), even though it isn’t strictly necessary:

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ We passed props
    console.log(props);      // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}

This ensures this.props is set even before the constructor exits.


There’s one last bit that longtime React users might be curious about.

You might have noticed that when you use the Context API in classes (either with the legacy contextTypes or the modern contextType API added in React 16.6), context is passed as a second argument to the constructor.

So why don’t we write super(props, context) instead? We could, but context is used less often so this pitfall just doesn’t come up as much.

With the class fields proposal this whole pitfall mostly disappears anyway. Without an explicit constructor, all arguments are passed down automatically. This is what allows an expression like state = {} to include references to this.props or this.context if necessary.

With Hooks, we don’t even have super or this. But that’s a topic for another day.


Discuss on Bluesky  ·  Edit on GitHub