DEV Community

Cover image for React - setState is not a function
Sagiv ben giat
Sagiv ben giat

Posted on • Originally published at debuggr.io

React - setState is not a function

Originally posted on my personal blog debugger.io

If you are a react developer and using a class component, you probably faced this error at least once:

TypeError: this.setState is not a function

TL;DR - If you are not in the mode for reading or you just want the bottom line, then here it is

Prerequisite -

  • We are going to mention the this context quite a lot here, if you are not exactly sure on how this works, i strongly advice reading JavaScript - The "this" key word in depth first.
  • We are also going to touch a little bit on the prototype chain subject, if you are not exactly sure how it works or not sure how classes works under the hood, i strongly advice reading JavaScript - The prototype chain in depth.

The problem

In order to understand what are the possible solutions, lets first understand what is the exact issue here.

Consider this code block:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  countUp() {
    this.setState(currentState => {
      return { count: currentState.count + 1 };
    });
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={this.countUp}>{count}</button>
      </div>
    );
  }
}

We have a component that manage a state with a counter property, we have a handler that is attach to the onClick of a <button> which will invoke the React.Component's setState method.

unfortunately, this won't work well. When the user click the button we will get an error:

TypeError: this.setState is not a function

We do use the extends React.Component which means we get access to all React.Component methods via this. So how come we can't invoke this.setState.

The real problem here, is that we "lost" the context of this inside that handler, or maybe not lost but its not pointing to where we think it should point.

What is the context of this in our case then?

Lets revisit our flow-chart from the JavaScript - The "this" key word in depth article:

this-flow-chart

Although there is no "event handlers" flow, we can place them under the "dot notation" or "object's member".

You can look at event handlers that are attached to DOM elements as if the function is a method inside the element’s object, in our case the button object. We can look at it as if we did button.click() or even button.countUp(). Note that this is not exactly whats going on under the hood, but this visualization of the invocation of the handler can help us with the formation of our “mental model” regarding the setting of this. You can read more about it on the MDN.

So what is this pointing to in our case? Lets walk through the flow:

  • Is countUp an arrow function? - No.
  • Was countUp called with new? - No.
  • Was countUp called with call / apply / bind? - No.
  • Was countUp called as an object method? - Yes (sort of), in our case the actual button is left to the dot, hence this is pointing to the button element.

This is why we have an error, because the button element does not have any setState method on it.

Possible solutions

#1 bind

One possible solution is to use bind and return a new function with an explicit this reference:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  countUp() {
    this.setState(currentState => {
      return { count: currentState.count + 1 };
    });
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={this.countUp.bind(this)}>{count}</button>
      </div>
    );
  }
}

This works great and we don't get any errors, although we are creating and passing a new function on each render cycle which may have performance implications (or might not).

We can use bind in the constructor which will run only once for the entire life-time of the component.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.countUp = this.countUp.bind(this);
  }

  countUp() {
    this.setState(currentState => {
      return { count: currentState.count + 1 };
    });
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={this.countUp}>{count}</button>
      </div>
    );
  }
}

This way, we are "overriding" the class method with an instance method, meaning we are not using the countUp method attached to the App.prototype but creating a method directly on the instance returned by App.

If you are not sure you fully understand how the prototype chain works under the hood, or not sure how classes works under the hood, i strongly recommend reading the JavaScript - The prototype chain in depth article

So why is using bind works for us? Lets walk through the flow again:

  • Is countUp an arrow function? - No.
  • Was countUp called with new? - No.
  • Was countUp called with call / apply / bind? - Yes.

Meaning, our this will reference whatever we pass to bind, which is the class instance.

#2 Arrow function

Instead of manually dealing with the this reference and passing it via bind, we can let the language / engine do it for us.

When using arrow functions, the engine will not "mutate" the this reference and will leave it as is, meaning whatever the this is pointing to at the wrapping execution context.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  countUp() {
    this.setState(currentState => {
      return { count: currentState.count + 1 };
    });
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={() => this.countUp()}>{count}</button>
      </div>
    );
  }
}

We are passing an inline arrow function and invoking this.countUp, this way the engine will not "mutate" our this reference thus it won't point to the button element, our function is called with a dot notation.

So lets walk through the flow again:

  • Is countUp an arrow function? - No.
  • Was countUp called with new? - No.
  • Was countUp called with call / apply / bind? - No.
  • Was countUp called as an object method? - Yes, this is the object left to the dot - The auto created object inside App in this case (the instance).

While this works great, we are again passing a new function on each render cycle, although it won't create any issues most of the time, you might want to create this function once. We can do that with class fields - Note that at the time this article was written, class fields are a proposal on stage 3.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  countUp = () => {
    this.setState(currentState => {
      return { count: currentState.count + 1 };
    });
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={this.countUp}>{count}</button>
      </div>
    );
  }
}

And if we are using class fields, why not remove the constructor and just declare the state as a class field?

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

  countUp = () => {
    this.setState(currentState => {
      return { count: currentState.count + 1 };
    });
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        <button onClick={this.countUp}>{count}</button>
      </div>
    );
  }
}

Now back to our this, why does it work with arrow functions? Lets walk through the flow again:

  • Is countUp an arrow function? - Yes. So whatever the this is in the wrapping context, which is the class instance.

Wrapping up

Make sure you don't "lose" the context of this in your handlers, either explicit pass it with bind (inline or override in constructor) or use an arrow function (inline or class field) that won't mutate and change the reference of this when it gets called.

I hope it was informative and helpful, if you have any further clarifications or corrections, feel free to comment or DM me on twitter (@sag1v). 🤓

For more articles you can visit debuggr.io

Top comments (1)

Collapse
 
sag1v profile image
Sagiv ben giat

Thanks! And i totally agree