React - setState() on unmounted component

JavascriptAjaxReactjsState

Javascript Problem Overview


In my react component im trying to implement a simple spinner while an ajax request is in progress - im using state to store the loading status.

For some reason this piece of code below in my React component throws this error

> Can only update a mounted or mounting component. This usually means > you called setState() on an unmounted component. This is a no-op. > Please check the code for the undefined component.

If I get rid of the first setState call the error goes away.

constructor(props) {
  super(props);
  this.loadSearches = this.loadSearches.bind(this);

  this.state = {
    loading: false
  }
}

loadSearches() {

  this.setState({
    loading: true,
    searches: []
  });

  console.log('Loading Searches..');

  $.ajax({
    url: this.props.source + '?projectId=' + this.props.projectId,
    dataType: 'json',
    crossDomain: true,
    success: function(data) {
      this.setState({
        loading: false
      });
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
      this.setState({
        loading: false
      });
    }.bind(this)
  });
}

componentDidMount() {
  setInterval(this.loadSearches, this.props.pollInterval);
}

render() {

    let searches = this.state.searches || [];


    return (<div>
          <Table striped bordered condensed hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Submit Date</th>
              <th>Dataset &amp; Datatype</th>
              <th>Results</th>
              <th>Last Downloaded</th>
            </tr>
          </thead>
          {
          searches.map(function(search) {

                let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
                let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
                let records = 0;
                let status = search.status ? search.status.toLowerCase() : ''

                return (
                <tbody key={search.id}>
                  <tr>
                    <td>{search.name}</td>
                    <td>{createdDate}</td>
                    <td>{search.dataset}</td>
                    <td>{records}</td>
                    <td>{downloadedDate}</td>
                  </tr>
                </tbody>
              );
          }
          </Table >
          </div>
      );
  }

The question is why am I getting this error when the component should already be mounted (as its being called from componentDidMount) I thought it was safe to set state once the component is mounted ?

Javascript Solutions


Solution 1 - Javascript

Without seeing the render function is a bit tough. Although can already spot something you should do, every time you use an interval you got to clear it on unmount. So:

componentDidMount() {
    this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}

componentWillUnmount () {
    this.loadInterval && clearInterval(this.loadInterval);
    this.loadInterval = false;
}

Since those success and error callbacks might still get called after unmount, you can use the interval variable to check if it's mounted.

this.loadInterval && this.setState({
    loading: false
});

Hope this helps, provide the render function if this doesn't do the job.

Cheers

Solution 2 - Javascript

> The question is why am I getting this error when the component should already be mounted (as its being called from componentDidMount) I thought it was safe to set state once the component is mounted ?

It is not called from componentDidMount. Your componentDidMount spawns a callback function that will be executed in the stack of the timer handler, not in the stack of componentDidMount. Apparently, by the time your callback (this.loadSearches) gets executed the component has unmounted.

So the accepted answer will protect you. If you are using some other asynchronous API that doesn't allow you to cancel asynchronous functions (already submitted to some handler) you could do the following:

if (this.isMounted())
     this.setState(...

This will get rid of the error message you report in all cases though it does feel like sweeping stuff under the rug, particularly if your API provides a cancel capability (as setInterval does with clearInterval).

Solution 3 - Javascript

To whom needs another option, the ref attribute's callback method can be a workaround. The parameter of handleRef is the reference to div DOM element.

For detailed information about refs and DOM: https://facebook.github.io/react/docs/refs-and-the-dom.html

handleRef = (divElement) => {
 if(divElement){
  //set state here
 }
}

render(){
 return (
  <div ref={this.handleRef}>
  </div>
 )
}

Solution 4 - Javascript

class myClass extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

    this.state = {
      data: [],
    };
  }

  componentDidMount() {
    this._isMounted = true;
    this._getData();
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  _getData() {
    axios.get('https://example.com')
      .then(data => {
        if (this._isMounted) {
          this.setState({ data })
        }
      });
  }

 
  render() {
    ...
  }
}

Solution 5 - Javascript

Share a solution enabled by react hooks.

React.useEffect(() => {
  let isSubscribed = true

  callApi(...)
    .catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
    .then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
    .catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))

  return () => (isSubscribed = false)
}, [])

the same solution can be extended to whenever you want to cancel previous requests on fetch id changes, otherwise there would be race conditions among multiple in-flight requests (this.setState called out of order).

React.useEffect(() => {
  let isCancelled = false

  callApi(id).then(...).catch(...) // similar to above

  return () => (isCancelled = true)
}, [id])

this works thanks to closures in javascript.

In general, the idea above was close to the makeCancelable approach recommended by the react doc, which clearly states > isMounted is an Antipattern

Credit

https://juliangaramendy.dev/use-promise-subscription/

Solution 6 - Javascript

For posterity,

This error, in our case, was related to Reflux, callbacks, redirects and setState. We sent a setState to an onDone callback, but we also sent a redirect to the onSuccess callback. In the case of success, our onSuccess callback executes before the onDone. This causes a redirect before the attempted setState. Thus the error, setState on an unmounted component.

Reflux store action:

generateWorkflow: function(
    workflowTemplate,
    trackingNumber,
    done,
    onSuccess,
    onFail)
{...

Call before fix:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    this.setLoading.bind(this, false),
    this.successRedirect
);

Call after fix:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    null,
    this.successRedirect,
    this.setLoading.bind(this, false)
);

More

In some cases, since React's isMounted is "deprecated/anti-pattern", we've adopted the use of a _mounted variable and monitoring it ourselves.

Solution 7 - Javascript

Just for reference. Using CPromise with decorators you can do the following tricks: (Live demo here)

export class TestComponent extends React.Component {
  state = {};

  @canceled(function (err) {
    console.warn(`Canceled: ${err}`);
    if (err.code !== E_REASON_DISPOSED) {
      this.setState({ text: err + "" });
    }
  })
  @listen
  @async
  *componentDidMount() {
    console.log("mounted");
    const json = yield this.fetchJSON(
      "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
    );
    this.setState({ text: JSON.stringify(json) });
  }

  @timeout(5000)
  @async
  *fetchJSON(url) {
    const response = yield cpFetch(url); // cancellable request
    return yield response.json();
  }

  render() {
    return (
      <div>
        AsyncComponent: <span>{this.state.text || "fetching..."}</span>
      </div>
    );
  }

  @cancel(E_REASON_DISPOSED)
  componentWillUnmount() {
    console.log("unmounted");
  }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMartyView Question on Stackoverflow
Solution 1 - JavascriptBruno MotaView Answer on Stackoverflow
Solution 2 - JavascriptMarcus Junius BrutusView Answer on Stackoverflow
Solution 3 - JavascriptburakView Answer on Stackoverflow
Solution 4 - Javascriptjohn_perView Answer on Stackoverflow
Solution 5 - JavascriptlxyyzView Answer on Stackoverflow
Solution 6 - JavascriptGeoffrey HaleView Answer on Stackoverflow
Solution 7 - JavascriptDmitriy MozgovoyView Answer on Stackoverflow