React - setState () auf nicht gemountete Komponente

91

In meiner Reaktionskomponente versuche ich, einen einfachen Spinner zu implementieren, während eine Ajax-Anforderung ausgeführt wird. Ich verwende den Status, um den Ladestatus zu speichern.

Aus irgendeinem Grund löst dieser Code in meiner React-Komponente diesen Fehler aus

Kann nur eine gemountete oder montierte Komponente aktualisieren. Dies bedeutet normalerweise, dass Sie setState () für eine nicht gemountete Komponente aufgerufen haben. Dies ist ein No-Op. Bitte überprüfen Sie den Code für die undefinierte Komponente.

Wenn ich den ersten setState-Aufruf los werde, verschwindet der Fehler.

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

Die Frage ist, warum ich diesen Fehler erhalte, wenn die Komponente bereits bereitgestellt werden sollte (wie sie von componentDidMount aufgerufen wird). Ich dachte, es wäre sicher, den Status festzulegen, sobald die Komponente bereitgestellt ist.

Marty
quelle
In meinem Konstruktor setze ich "this.loadSearches = this.loadSearches.bind (this);" - Ich werde das der Frage hinzufügen
Marty
Haben Sie versucht, das Laden in Ihrem Konstruktor auf null zu setzen? Das könnte funktionieren. this.state = { loading : null };
Pramesh Bajracharya

Antworten:

69

Ohne zu sehen, ist die Renderfunktion etwas schwierig. Obwohl Sie bereits etwas erkennen können, das Sie tun sollten, müssen Sie es jedes Mal, wenn Sie ein Intervall verwenden, beim Aufheben löschen. So:

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

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

Da diese Erfolgs- und Fehlerrückrufe nach dem Aufheben der Bereitstellung möglicherweise immer noch aufgerufen werden, können Sie mithilfe der Intervallvariablen überprüfen, ob sie bereitgestellt ist.

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

Hoffe das hilft, stelle die Renderfunktion zur Verfügung, wenn dies nicht funktioniert.

Prost

Bruno Mota
quelle
2
Bruno, könntest du nicht einfach die Existenz von "diesem" Kontext
testen?
6
Oder einfach:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz
@GregHerbowicz Wenn Sie die Komponente mit dem Timer aushängen und montieren, kann sie auch dann noch ausgelöst werden, wenn Sie das einfache Löschen durchführen.
Corlaez
14

Die Frage ist, warum ich diesen Fehler erhalte, wenn die Komponente bereits bereitgestellt werden sollte (wie sie von componentDidMount aufgerufen wird). Ich dachte, es wäre sicher, den Status festzulegen, sobald die Komponente bereitgestellt ist.

Es wird nicht von aufgerufen componentDidMount. Sie componentDidMounterzeugen eine Rückruffunktion, die im Stapel des Timer-Handlers ausgeführt wird, nicht im Stapel von componentDidMount. Anscheinend hat this.loadSearchesdie Komponente zum Zeitpunkt der Ausführung von callback ( ) die Bereitstellung aufgehoben.

Die akzeptierte Antwort schützt Sie also. Wenn Sie eine andere asynchrone API verwenden, mit der Sie keine asynchronen Funktionen abbrechen können (die bereits an einen Handler gesendet wurden), können Sie Folgendes tun:

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

Dadurch wird die von Ihnen gemeldete Fehlermeldung in allen Fällen beseitigt, obwohl Sie das Gefühl haben, Dinge unter den Teppich zu kehren, insbesondere wenn Ihre API eine Abbruchfunktion bietet (wie setIntervalbei clearInterval).

Marcus Junius Brutus
quelle
12
isMountedist ein Antimuster, von dem Facebook rät, es nicht zu verwenden: facebook.github.io/react/blog/2015/12/16/…
Marty
1
Ja, ich sage, dass "es sich anfühlt, als würde man Sachen unter den Teppich kehren".
Marcus Junius Brutus
5

Für wen eine andere Option benötigt wird, kann die Rückrufmethode des ref-Attributs eine Problemumgehung sein. Der Parameter von handleRef ist die Referenz auf das div DOM-Element.

Ausführliche Informationen zu Refs und DOM finden Sie unter 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>
 )
}
Burakhan Alkan
quelle
5
Die Verwendung eines Verweises für effektiv "isMounted" ist genau das Gleiche wie die Verwendung von isMounted, jedoch weniger klar. isMounted ist aufgrund seines Namens kein Anti-Pattern, sondern ein Anti-Pattern, das Verweise auf eine nicht gemountete Komponente enthält.
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() {
    ...
  }
}
john_per
quelle
Gibt es eine Möglichkeit, dies für eine Funktionskomponente zu erreichen? @john_per
Tamjid
Für eine Funktionskomponente würde ich ref verwenden: const _isMounted = useRef (false); @ Tamjid
john_per
1

Für die Nachwelt

Dieser Fehler war in unserem Fall auf Reflux, Rückrufe, Weiterleitungen und setState zurückzuführen. Wir haben einen setState an einen onDone-Rückruf gesendet, aber wir haben auch eine Umleitung an den onSuccess-Rückruf gesendet. Im Erfolgsfall wird unser onSuccess-Rückruf vor dem onDone ausgeführt . Dies führt zu einer Umleitung vor dem versuchten setState . Daher der Fehler setState für eine nicht gemountete Komponente.

Reflux-Speicheraktion:

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

Vor dem Fix anrufen:

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

Rufen Sie nach dem Fix an:

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

Mehr

In einigen Fällen haben wir, da Reacts isMounted "veraltet / anti-pattern" ist, die Verwendung einer _mounted-Variablen übernommen und diese selbst überwacht.

Geoffrey Hale
quelle
0

Teilen Sie eine Lösung, die durch React Hooks aktiviert wird .

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

Dieselbe Lösung kann erweitert werden, wenn Sie frühere Anforderungen bei Änderungen der Abruf-ID stornieren möchten. Andernfalls gibt es Rennbedingungen zwischen mehreren Anfragen this.setStatewährend des Flugs ( nicht in der richtigen Reihenfolge aufgerufen).

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

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

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

Dies funktioniert dank Verschlüssen in Javascript.

Im Allgemeinen entsprach die obige Idee dem vom React Doc empfohlenen makeCancelable-Ansatz , in dem eindeutig angegeben ist

isMounted ist ein Antipattern

Anerkennung

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

Xlee
quelle