99

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 ?

Marty
  • 2,965
  • 4
  • 30
  • 45
  • in my constructor i am setting "this.loadSearches = this.loadSearches.bind(this);" - ill add that to the question – Marty Oct 02 '15 at 08:22
  • have you tried setting **loading** to null in your constructor? That might work. `this.state = { loading : null };` – Pramesh Bajracharya Feb 22 '18 at 09:00
  • Hi, I know this is a very old post.. but just to update on the latest development : the setstate warning has been removed from the React codebase(See [PR](https://github.com/facebook/react/pull/22114)). The reason being.. 1. They are false positives in some cases 2. Avoiding false positives leads to people adopting undesirable code patterns that are less readable 3. React will "offer a feature that lets you preserve DOM and state, even when the component is not visible", and some code patterns adopted to just get by the setState warning may lead to undesired behaviors in the future. – Katie Dec 10 '21 at 06:09

7 Answers7

70

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

Bruno Mota
  • 823
  • 7
  • 6
13

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).

Marcus Junius Brutus
  • 26,087
  • 41
  • 189
  • 331
6

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>
 )
}
burak
  • 3,839
  • 1
  • 15
  • 20
  • 7
    Using a ref for effectively "isMounted" is exactly the same thing as just using isMounted but less clear. isMounted isn't an anti-pattern because of its name but because it is an anti-pattern to hold references to an unmounted component. – Pajn Oct 17 '17 at 07:52
3
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() {
    ...
  }
}
john_per
  • 320
  • 6
  • 16
3

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/

Allen
  • 4,431
  • 2
  • 27
  • 39
1

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.

Geoffrey Hale
  • 10,597
  • 5
  • 44
  • 45
0

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");
  }
}
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7