116 votes

Comment annuler un fetch dans componentWillUnmount

Je pense que le titre dit tout. L'avertissement jaune s'affiche à chaque fois que je démonte un composant qui est encore en cours de récupération.

Console

Avertissement: Impossible d'appeler setState (ou forceUpdate) sur un composant démonté. C'est une opération nulle, mais ... Pour corriger, annuler toutes les abonnements et tâches asynchrones dans la méthode componentWillUnmount.

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'En cours de chargement...',
        id: 'en_cours_de_chargement',
      }]
    }
  }

  componentDidMount(){
    return fetch('LIEN ICI')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }

0 votes

Quel est cet avertissement je n'ai pas ce problème

0 votes

Question mise à jour

0 votes

Avez-vous promis du code asynchrone pour la récupération

118voto

Tomasz Mularczyk Points 12030

Lorsque vous déclenchez une Promise, il peut s'écouler quelques secondes avant qu'elle ne se résolve et d'ici là, l'utilisateur peut avoir navigué vers un autre endroit de votre application. Ainsi, lorsque la Promise se résout, setState est exécuté sur un composant non monté et vous obtenez une erreur - tout comme dans votre cas. Cela peut également entraîner des fuites de mémoire.

C'est pourquoi il est préférable de déplacer une partie de votre logique asynchrone en dehors des composants.

Sinon, vous devrez d'une manière ou d'une autre annuler votre Promise. En alternative - en dernier recours (c'est un antipattern) - vous pouvez conserver une variable pour vérifier si le composant est toujours monté :

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

Je vais de nouveau souligner que c'n'est un antipattern mais cela peut être suffisant dans votre cas (tout comme ils l'ont fait avec l'implémentation de Formik)

.

Une discussion similaire sur GitHub

EDIT:

C'est probablement comment je résoudrais le même problème (avec rien d'autre que React) en utilisant Hooks:

OPTION A:

import React, { useState, useEffect } from "react";

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    {value ? value : "récupération des données..."}
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // suivre si le composant est monté

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // nettoyage
      isMounted = false;
    };
  }, []); // uniquement sur "didMount"

  return value;
}

OPTION B: Alternativement avec useRef qui se comporte comme une propriété statique d'une classe, ce qui signifie qu'elle ne déclenche pas de nouveau rendu du composant lors de son changement de valeur :

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// ou l'extraire pour le custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted; // retourner "isMounted.current" ne fonctionnerait pas car nous retournerions un primitif non mutable
}

Exemple: https://codesandbox.io/s/86n1wq2z8

5 votes

Alors, il n'y a vraiment aucun moyen d'annuler simplement la récupération dans le componentWillUnmount ?

0 votes

@JoãoBelo Je ne suis pas sûr si c'est pris en charge. Jetez un coup d'œil à ce package

1 votes

Oh, je n'avais pas remarqué le code de votre réponse avant, ça a fonctionné. Merci

31voto

haleonj Points 69

Les gens sympathiques chez React recommandent d'envelopper vos appels fetch/promises dans une promise annulable. Bien qu'il n'y ait aucune recommandation dans cette documentation pour garder le code séparé de la classe ou de la fonction avec le fetch, cela semble conseillé car d'autres classes et fonctions sont susceptibles d'avoir besoin de cette fonctionnalité, la duplication de code est un anti-pattern, et de toute façon le code persistant doit être détruit ou annulé dans componentWillUnmount(). Selon React, vous pouvez appeler cancel() sur la promise enveloppée dans componentWillUnmount pour éviter de définir un état sur un composant non monté.

Le code fourni ressemblerait à quelque chose comme ces extraits de code si nous utilisons React comme guide:

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

---- EDIT ----

J'ai trouvé que la réponse donnée n'était peut-être pas tout à fait correcte en suivant le problème sur GitHub. Voici une version que j'utilise qui fonctionne pour mes besoins:

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

L'idée était d'aider le collecteur de déchets à libérer de la mémoire en rendant la fonction ou tout ce que vous utilisez null.

0 votes

Avez-vous le lien vers le problème sur Github ?

0 votes

@Ren, il y a un site GitHub pour modifier la page et discuter des problèmes.

0 votes

Je ne suis plus sûr de l'endroit exact du problème sur ce projet GitHub.

27voto

Paduado Points 151

Vous pouvez utiliser AbortController pour annuler une requête fetch.

Voir aussi: https://www.npmjs.com/package/abortcontroller-polyfill

class FetchComponent extends React.Component{
  state = { todos: [] };

  controller = new AbortController();

  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }

  componentWillUnmount(){
    this.controller.abort();
  }

  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };

  componentDidMount(){
    this.setState({ fetch: false });
  }

  render(){
    return this.state.fetch && 
  }
}

ReactDOM.render(, document.getElementById('root'))

2 votes

Je souhaite avoir su qu'il existe une API Web pour annuler des requêtes comme AbortController. Mais bon, ce n'est pas trop tard pour le savoir. Merci.

1 votes

Donc, si vous avez plusieurs fetch, pouvez-vous passer ce seul AbortController à tous ?

0 votes

Peut-être, chacun de .then() devrait inclure également la vérification : if (this.controller.signal.abored) return Promise.reject('Aborted');

16voto

Ben Yitzhaki Points 26

Depuis que la publication a été ouverte, un "abortable-fetch" a été ajouté. https://developers.google.com/web/updates/2017/09/abortable-fetch

(à partir de la documentation:)

La manoeuvre + signal du contrôleur Rencontrez AbortController et AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

Le contrôleur n'a qu'une seule méthode:

controller.abort(); Lorsque vous faites cela, il notifie le signal:

signal.addEventListener('abort', () => {
  // Logs true:
  console.log(signal.aborted);
});

Cette API est fournie par la norme du DOM, et c'est toute l'API. Elle est délibérément générique pour qu'elle puisse être utilisée par d'autres normes web et bibliothèques JavaScript.

par exemple, voici comment vous pourriez définir une limite de temps pour une requête fetch après 5 secondes:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
});

1 votes

Intéressant, je vais essayer cette méthode. Mais avant cela, je vais lire d'abord l'API AbortController.

1 votes

Pouvons-nous utiliser juste une instance d'AbortController pour plusieurs fetches de sorte que lorsque nous invoquons la méthode d'annulation de ce seul AbortController dans componentWillUnmount, cela annulera tous les fetches existants dans notre composant ? Sinon, cela signifie que nous devons fournir différentes instances d'AbortController pour chacun des fetches, n'est-ce pas ?

0 votes

@LexSoft as-tu trouvé une réponse à ta question?

2voto

Le cœur de cet avertissement est que votre composant a une référence à celui-ci qui est maintenue par un rappel/promise en cours.

Pour éviter l'anti-modèle de garder votre état isMounted (qui maintient votre composant en vie) comme cela a été fait dans le deuxième modèle, le site web de react suggère d'utiliser une promise facultative; cependant, ce code semble également maintenir votre objet en vie.

À la place, je l'ai fait en utilisant une fermeture avec une fonction liée imbriquée pour setState.

Voici mon constructeur (typescript)...

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // il est important que ce soit un niveau inférieur, de sorte que nous puissions supprimer la
        // référence à l'objet entier en le définissant sur undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // idéalement, nous aimerions avoir un chaînage facultatif
        // cancelable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // supprimer toutes les références.
    }
}

3 votes

Il n'y a pas de différence conceptuelle avec la conservation d'un indicateur isMounted, sauf que vous le liez à la fermeture (closure) au lieu de le suspendre de this.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X