React - setState () sur un composant non monté


92

Dans mon composant de réaction, j'essaie d'implémenter un simple spinner pendant qu'une requête ajax est en cours - j'utilise l'état pour stocker l'état de chargement.

Pour une raison quelconque, ce morceau de code ci-dessous dans mon composant React lève cette erreur

Peut uniquement mettre à jour un composant monté ou de montage. Cela signifie généralement que vous avez appelé setState () sur un composant non monté. C'est un no-op. Veuillez vérifier le code du composant non défini.

Si je me débarrasse du premier appel setState, l'erreur disparaît.

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 question est pourquoi j'obtiens cette erreur alors que le composant devrait déjà être monté (comme il est appelé à partir de componentDidMount) J'ai pensé qu'il était sûr de définir l'état une fois que le composant est monté?


dans mon constructeur, je suis en train de définir "this.loadSearches = this.loadSearches.bind (this);" - mal ajouter cela à la question
Marty

avez-vous essayé de définir le chargement sur null dans votre constructeur? Cela pourrait marcher. this.state = { loading : null };
Pramesh Bajracharya

Réponses:


69

Sans voir la fonction de rendu, c'est un peu dur. Bien que vous puissiez déjà repérer quelque chose que vous devriez faire, chaque fois que vous utilisez un intervalle, vous devez l'effacer au démontage. Alors:

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

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

Étant donné que ces rappels de succès et d'erreur peuvent toujours être appelés après le démontage, vous pouvez utiliser la variable d'intervalle pour vérifier si elle est montée.

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

J'espère que cela aide, fournissez la fonction de rendu si cela ne fait pas le travail.

À votre santé


2
Bruno, ne pourriez-vous pas simplement tester l'existence de "ce" contexte ... ala this && this.setState .....
james emanon

6
Ou simplement:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz

@GregHerbowicz si vous démontez et montez le composant avec la minuterie, il peut toujours être déclenché même si vous effectuez le nettoyage simple.
corlaez

14

La question est pourquoi j'obtiens cette erreur alors que le composant devrait déjà être monté (comme il est appelé à partir de componentDidMount) J'ai pensé qu'il était sûr de définir l'état une fois que le composant est monté?

Il n'est pas appelé depuis componentDidMount. Votre componentDidMountgénère une fonction de rappel qui sera exécutée dans la pile du gestionnaire de minuterie, et non dans la pile de componentDidMount. Apparemment, au moment où votre callback ( this.loadSearches) est exécuté, le composant s'est démonté.

Ainsi, la réponse acceptée vous protégera. Si vous utilisez une autre API asynchrone qui ne vous permet pas d'annuler les fonctions asynchrones (déjà soumises à un gestionnaire), vous pouvez effectuer les opérations suivantes:

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

Cela éliminera le message d'erreur que vous signalez dans tous les cas, même si cela donne l'impression de balayer des choses sous le tapis, en particulier si votre API fournit une capacité d'annulation (comme le setIntervalfait clearInterval).


12
isMountedest un anti-modèle que facebook conseille de ne pas utiliser: facebook.github.io/react/blog/2015/12/16/…
Marty

1
Oui, je dis que "on a l'impression de balayer des choses sous le tapis".
Marcus Junius Brutus

5

Pour qui a besoin d'une autre option, la méthode de rappel de l'attribut ref peut être une solution de contournement. Le paramètre de handleRef est la référence à l'élément DOM div.

Pour des informations détaillées sur les refs et 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
Utiliser une référence pour effectivement "isMounted" est exactement la même chose que simplement utiliser isMounted mais moins clair. isMounted n'est pas un anti-pattern à cause de son nom mais parce que c'est un anti-pattern pour contenir des références à un composant non monté.
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() {
    ...
  }
}

Existe-t-il un moyen d'y parvenir pour un composant fonctionnel? @john_per
Tamjid

Pour un composant de fonction, j'utiliserais ref: const _isMounted = useRef (false); @Tamjid
john_per

1

Pour la postérité,

Cette erreur, dans notre cas, était liée à Reflux, aux rappels, aux redirections et à setState. Nous avons envoyé un setState à un rappel onDone, mais nous avons également envoyé une redirection vers le rappel onSuccess. En cas de succès, notre rappel onSuccess s'exécute avant le onDone . Cela provoque une redirection avant la tentative setState . Ainsi l'erreur, setState sur un composant non monté.

Action du magasin de reflux:

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

Appelez avant la correction:

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

Appel après correction:

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

Plus

Dans certains cas, puisque isMounted de React est "obsolète / anti-pattern", nous avons adopté l'utilisation d'une variable _mounted et la surveillons nous-mêmes.


1

Partagez une solution activée par les hooks de réaction .

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 même solution peut être étendue à chaque fois que vous souhaitez annuler les demandes précédentes sur les changements d'id de récupération, sinon il y aurait des conditions de concurrence entre plusieurs demandes en cours ( this.setStateappelées dans le désordre).

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

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

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

cela fonctionne grâce aux fermetures en javascript.

En général, l'idée ci-dessus était proche de l' approche makeCancelable recommandée par le react doc, qui énonce clairement

isMounted est un Antipattern

Crédit

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.