React - setState () sul componente non montato


92

Nel mio componente React sto cercando di implementare un semplice spinner mentre è in corso una richiesta ajax - sto usando lo stato per memorizzare lo stato di caricamento.

Per qualche motivo questo pezzo di codice qui sotto nel mio componente React genera questo errore

Può aggiornare solo un componente montato o di montaggio. Questo di solito significa che hai chiamato setState () su un componente non montato. Questa è una no-op. Controlla il codice per il componente non definito.

Se mi sbarazzo della prima chiamata a setState l'errore scompare.

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>
      );
  }

La domanda è: perché ricevo questo errore quando il componente dovrebbe già essere montato (poiché viene chiamato da componentDidMount). Ho pensato che fosse sicuro impostare lo stato una volta montato il componente?


nel mio costruttore sto impostando "this.loadSearches = this.loadSearches.bind (this);" - aggiungo questo alla domanda
Marty

hai provato a impostare il caricamento su null nel tuo costruttore? Potrebbe funzionare. this.state = { loading : null };
Pramesh Bajracharya

Risposte:


69

Senza vedere la funzione di rendering è un po 'difficile. Anche se puoi già individuare qualcosa che dovresti fare, ogni volta che usi un intervallo devi cancellarlo allo smontaggio. Così:

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

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

Poiché tali callback di successo ed errore potrebbero ancora essere richiamati dopo lo smontaggio, è possibile utilizzare la variabile interval per verificare se è montata.

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

Spero che questo aiuti, fornisci la funzione di rendering se questo non funziona.

Saluti


2
Bruno, non potresti semplicemente provare l'esistenza di "questo" contesto .. ala this && this.setState .....
james emanon

6
O semplicemente:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz

@GregHerbowicz se stai smontando e montando il componente con il timer, questo può comunque essere attivato anche se fai la semplice cancellazione.
corlaez

14

La domanda è: perché ricevo questo errore quando il componente dovrebbe già essere montato (poiché viene chiamato da componentDidMount). Ho pensato che fosse sicuro impostare lo stato una volta montato il componente?

E ' non è chiamato da componentDidMount. Il tuo componentDidMountgenera una funzione di callback che verrà eseguita nello stack del gestore del timer, non nello stack di componentDidMount. Apparentemente, nel momento in cui callback ( this.loadSearches) viene eseguito il componente è smontato.

Quindi la risposta accettata ti proteggerà. Se stai utilizzando qualche altra API asincrona che non ti consente di annullare le funzioni asincrone (già inviate a qualche gestore) potresti fare quanto segue:

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

Questo eliminerà il messaggio di errore che segnali in tutti i casi, anche se sembra come spazzare via le cose sotto il tappeto, in particolare se la tua API fornisce una capacità di cancellazione (come setIntervalfa con clearInterval).


12
isMountedè un antipattern che facebook consiglia di non usare: facebook.github.io/react/blog/2015/12/16/…
Marty

1
Sì, dico che "è come spazzare roba sotto il tappeto".
Marcus Junius Brutus

5

Per chi ha bisogno di un'altra opzione, il metodo di callback dell'attributo ref può essere una soluzione alternativa. Il parametro di handleRef è il riferimento all'elemento div DOM.

Per informazioni dettagliate su ref e 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>
 )
}

5
Usare un riferimento per "isMounted" in modo efficace è esattamente la stessa cosa che usare solo isMounted ma meno chiaro. isMounted non è un anti-pattern a causa del suo nome ma perché è un anti-pattern per contenere riferimenti a un componente non montato.
Pajn

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() {
    ...
  }
}

C'è un modo per ottenere questo risultato per un componente funzionale? @john_per
Tamjid

Per un componente funzione userei ref: const _isMounted = useRef (false); @Tamjid
john_per

1

Per i posteri,

Questo errore, nel nostro caso, era correlato a Reflux, callback, reindirizzamenti e setState. Abbiamo inviato un setState a una richiamata onDone, ma abbiamo anche inviato un reindirizzamento alla richiamata onSuccess. In caso di successo, la nostra richiamata onSuccess viene eseguita prima di onDone . Ciò causa un reindirizzamento prima del tentativo di setState . Quindi l'errore, setState su un componente smontato.

Azione del negozio di reflusso:

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

Chiama prima della correzione:

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

Chiamata dopo correzione:

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

Di Più

In alcuni casi, poiché isMounted di React è "deprecato / anti-pattern", abbiamo adottato l'uso di una variabile _mounted e monitorandola noi stessi.


1

Condividi una soluzione abilitata dagli hook di reazione .

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)
}, [])

la stessa soluzione può essere estesa a ogni volta che si desidera annullare le richieste precedenti su modifiche dell'ID di recupero, altrimenti ci sarebbero condizioni di gara tra più richieste in volo ( this.setStatechiamate fuori servizio).

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

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

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

questo funziona grazie alle chiusure in javascript.

In generale, l'idea sopra era vicina all'approccio makeCancelable raccomandato dal documento di react, che afferma chiaramente

isMounted è un Antipattern

Credito

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.