30

Diagnosing React’s “Cannot update during an existing state transition” Error

 4 years ago
source link: https://www.tuicool.com/articles/E36RraJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Just the other day I came across an interesting error in React, despite my code working as it should. Here’s the error:

“Cannot update during an existing state transition”

Not super confusing and certainly not one of those ‘what in the world does this mean’ sort of errors, but not the most helpful either. However, React normally logs some helpful information in the console log with the offending file and the whereabouts of the error.

As it happens, I’d made a silly mistake and was basically trying to update state inside of React’s render() method: a rookie error for sure and one that is easily made when you take your eye off the ball.

It was working on an HTML form and using the recommended approach of using controlled components . At the same time as letting React manage and update my inputs’ state though, I wanted to set an initial value that could be passed in via props. And that’s where the trouble began.

Why set an initial value in a controlled input?

There are quite a few reasons, but in this case I was working on a login form that retrieved a username from some sort of storage (local storage in the browser, a cookie, or a query string, etc.) and helpfully entered it into the username text input to save the returning user having to enter it again.

The problem comes when trying to inject this initial value into a component that is managing its own state for an input’s value. If you try to update state in the wrong place (i.e. in the render() method) then you usually end up in an infinite loop. This wasn’t happening to me, the component was loading, but I still got the slightly weird ‘cannot update during an existing state transition’ error in the console.

What is the “Cannot update during an existing state transition” error in React?

When you try to update state inside of the render() method what happens is a recursion or infinite loop. Essentially you’re trying to update state, which in turn triggers a re-rendering, which tries to set state again, which triggers the re-rendering, which sets state…see where this is going?

It normally looks like this:

63YNvuY.png!web

With the ‘cannot update during an existing state transition’ error, it’s a similar problem in that state is in-flux — it’s being changed — but the component is trying to update and render nevertheless. This is why you’ll usually see everything rendering and working on the front end, but you’ll get this peculiar error in the console. That, and the component might not work exactly how you expect. It looks like this:

biuMJzq.png!web

Updating controlled inputs with initial values, step by step

With some background out of the way, a concrete example will be helpful to nail down the concept of why updating state mid-render is less than ideal and how to deal with this sort of scenario. I’ve created a mini project on CodeSandbox that you can check out and I’ll embed the full thing towards the end of the article.

First up is our container App, which kicks off the project; App lives in the index.js file:

import React, { Component } from "react";
import ReactDOM from "react-dom";

import "./styles.css";
import Fixed from "./fixed";
import Error from "./error";

class App extends Component {
  displayType = {
    FIXED: "fixed",
    ERROR: "error"
  };

  state = {
    display: ""
  };

  switchComponents = component => {
    this.setState({
      display: component
    });
  };

  getQSValue = name => {
    const search = window.location.search;
    const params = new URLSearchParams(search);
    return params.get(name) || "";
  };

  render() {
    return (
      <div className="App">
        <h1>Hello React fans</h1>
        <h2>
          An example of the quasi-mysterious "Cannot update during an existing
          state transition" error
        </h2>
        <p>
          <button onClick={() => this.switchComponents(this.displayType.ERROR)}>
            Show the component with the error
          </button>
          <button onClick={() => this.switchComponents(this.displayType.FIXED)}>
            Show the fixed component
          </button>
        </p>
        <div>
          {this.state.display === this.displayType.ERROR ? (
            <Error initialValue={this.getQSValue("value")} />
          ) : null}
          {this.state.display === this.displayType.FIXED ? (
            <Fixed initialValue={this.getQSValue("value")} />
          ) : null}
        </div>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Nothing too fancy here. The App component renders a title and two buttons, one to show the working component with correct loading of state, and one that triggers the error. Inside App, each button triggers a state switch to toggle the Fixed or Error component. We also have a `getQSValue()` method, which looks for a named key in the query string and returns it. We use this to pass a prop into our Fixed and Error components which will attempt to set our inputs’ value property.

All things being equal, this is the result:

26VvIru.png!web

Stunning, I know. But you can see that I clicked the ‘Show the fixed component’ button and we can enter a name in the text box, where our component will output it into our ‘Hi [value], that’s a nice name’ string.

Let’s investigate the Error component to see how it goes wrong…

The Error Component

Here’s how our Error component started off:

QraueeI.png!web

Innocent enough, but you can see that right at the beginning of the render() method, we’re updating state with that prop I mentioned earlier, ‘initialValue’. Sadly, this causes out infinite loop error, killing the App:

63YNvuY.png!web

Fixing the Error component

If our problem is with the state being called too many times, what if we improved it so that we only attempted to set state once, and then added a flag that would skip over this part in future?

Well, that would look like this:

jUj2EjU.png!web

You can see we’ve added an extra flag/boolean field to state called ‘initialValueSet’. Now, when we come to render our component, we’ve updated the setState method to check for a few things to determine if we should update state at all:

  • We check if state has already been set using the ‘initialValueSet’ flag
  • Then we’ll look to see if the value is empty or not
  • Finally, we’ll check if the props.initialValue is even available

When we run the code now, everything appears to work OK on the front end. Our app loads and it works much the same as the Fixed component.

uqaayeR.png!web

Notice that we’ve supplied the query string value ‘value’ as ‘thea’ and this has been set in our Error component, updating state once and then skipping over the recursion error, setting out input value to ‘thea’. However, our console error is present:

biuMJzq.png!web

Writing the Fixed component

With the Error component left alone, everything works sort of, suspiciously fine. However, the code is clunky with the multi-conditional IF statement and we’re still getting errors. A better approach will be to rewrite the component’s handling of initial value and take care to keep the render() method pure, leveraging React’s lifecycle to do the work for us. You can find a great React lifecycle cheatsheet here .

To update our Fixed component, we’re going revert back to our original Error component’s code but this time, we’ll move the initialValue handling into React’s built-in componentWillMount() lifecycle method. This way, even if we cause a re-render, the render method is kept pure and we don’t have any state mismatching or recursion errors.

componentWillMount() {
    if (this.props.initialValue) {
      this.setState({
        value: this.props.initialValue
      });
    }
  }

Note:It’s worth mentioning that the componentWillMount() method is in the process of being deprecated with a view to removing it altogether as of version 17. You can use it fine for now, but to future-proof your code, you could also move this piece of code to the `componentDidMount()` method, or the constructor and you’d get the same result.

Now, if you run the app and click the ‘Show the Fixed component’, we get:

QBNvIfq.gif

No errors, working code, loading an initial value from a querystring; happy days indeed.

Viewing the full project

If you’d like to check out the full React project on CodeSandbox then go ahead. You can also browse the embedded project below:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK