web-dev-qa-db-fra.com

Comment annuler une extraction sur le composantWillUnmount

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

Avertissement: Impossible d'appeler setState (ou forceUpdate) sur un composant non monté. Ceci est un non-op, mais ... Pour résoudre ce problème, annulez tous les souscriptions et tâches asynchrones dans la méthode componentWillUnmount.

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

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }
68
João Belo

Lorsque vous tenez une promesse, cela peut prendre quelques secondes avant que celle-ci soit résolue et, à ce moment-là, l'utilisateur pourrait avoir navigué vers un autre endroit de votre application. Ainsi, lorsque Promise résout setState est exécuté sur un composant non monté et que vous obtenez une erreur - comme dans votre cas. Cela peut également provoquer des fuites de mémoire.

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

Sinon, vous devrez d’une manière ou d’une autre annuler votre promesse . Alternativement - 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 soulignerai encore une fois - ceci est un antipattern mais peut suffire dans votre cas (tout comme ils l’ont fait avec Formik implémentation).

Une discussion similaire sur GitHub

EDIT:

Voici probablement comment résoudre le même problème (n'ayant que React) avec Hooks :

OPTION A:

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

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

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

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

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

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

OPTION B: Ou bien avec useRef qui se comporte comme une propriété statique d'une classe, ce qui signifie qu'il ne rend pas le composant restitué lorsque sa valeur change:

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

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

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

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

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

51
Tomasz Mularczyk

Les sympathiques personnes à React recommendé emballent vos appels/promesses de recherche dans une promesse pouvant être annulée. Bien que cette documentation ne recommande pas de séparer le code de la classe ou de la fonction avec l'extraction, cela semble souhaitable, car les autres classes et fonctions auront probablement besoin de cette fonctionnalité, la duplication de code est un anti-motif et quel que soit le code en cours doit être éliminé ou annulé dans componentWillUnmount(). Selon React, vous pouvez appeler cancel() sur la promesse emballée dans componentWillUnmount pour éviter de définir l'état sur un composant non monté.

Le code fourni ressemblerait à ces extraits de code si nous utilisions 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();
}

---- MODIFIER ----

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 ramasse-miettes à libérer de la mémoire en rendant la fonction ou tout ce que vous utilisez null.

16
haleonj

Vous pouvez utiliser AbortController pour annuler une demande d’extraction.

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 && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
12
Paduado

Depuis l'ouverture du poste, un "abortable-fetch" a été ajouté. https://developers.google.com/web/updates/2017/09/abortable-fetch

(des docs :)

La manette + signal manoeuvre Rencontrez AbortController et AbortSignal:

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

Le contrôleur n'a qu'une 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 le standard DOM et constitue l'intégralité de l'API. Il est délibérément générique pour pouvoir être utilisé par d'autres standards Web et bibliothèques JavaScript.

par exemple, voici comment créer un délai de récupération 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);
});
8
Ben Yitzhaki

Le cœur de cet avertissement est que votre composant possède une référence qui est détenue par une promesse/rappel en attente.

Pour éviter l'effet de maintien de l'état isMounted (ce qui maintient votre composant actif), comme cela a été fait dans le deuxième modèle, le site Web de réaction suggère en utilisant une promesse facultative ; Cependant, ce code semble également conserver votre objet en vie.

Au lieu de cela, je l'ai fait en utilisant une fermeture avec une fonction liée imbriquée à setState.

Voici mon constructeur (TypeScript)…

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

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}
3
Anthony Wieser

Lorsque je dois "annuler tous les abonnements et les processus asynchrones", j'envoie habituellement quelque chose à redux dans composantWillUnmount afin d'informer tous les autres abonnés et d'envoyer une autre demande d'annulation au serveur, si nécessaire.

3
Sasha Kos