Je pense que le titre dit tout. L'avertissement jaune s'affiche chaque fois que je démonte un composant en cours de récupération.
ConsoleAvertissement: impossible d'appeler
setState
(ouforceUpdate
) sur un composant non monté. Ce n'est pas une opération, mais ... Pour corriger, annulez tous les abonnements et les tâches asynchrones dans lacomponentWillUnmount
méthode.
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);
});
}
Réponses:
Lorsque vous lancez une promesse, la résolution de la promesse peut prendre quelques secondes et à ce moment-là, l'utilisateur peut avoir navigué vers un autre endroit de votre application. Ainsi, lorsque Promise résout
setState
est exécuté sur un composant non monté et 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 des composants.
Sinon, vous devrez annuler d'une manière ou d'une autre votre promesse . Alternativement - en dernier recours (c'est un anti-modèle) - 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 le soulignerai encore une fois - c'est un anti-modèle mais peut être suffisant dans votre cas (tout comme ils l'ont fait avec la
Formik
mise en œuvre).Une discussion similaire sur GitHub
ÉDITER:
C'est probablement ainsi que je résoudrais 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: Alternativement avec
useRef
qui se comporte comme une propriété statique d'une classe, ce qui signifie qu'il ne rend pas le composant 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
la source
Les gens sympathiques de React recommandent d' encapsuler vos appels / promesses de récupération dans une promesse annulable. Bien qu'il n'y ait aucune recommandation dans cette documentation de garder le code séparé de la classe ou de la fonction avec l'extraction, cela semble souhaitable car d'autres classes et fonctions auront probablement besoin de cette fonctionnalité, la duplication de code est un anti-modèle, et quel que soit le code persistant doit être éliminé ou annulé dans
componentWillUnmount()
. Comme pour React, vous pouvez appelercancel()
la promesse encapsuléecomponentWillUnmount
pour éviter de définir l'état d'un composant non monté.Le code fourni ressemblerait à 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(); }
---- ÉDITER ----
J'ai trouvé que la réponse donnée peut ne pas être 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 garbage collector à libérer de la mémoire en rendant la fonction ou tout ce que vous utilisez nul.
la source
Vous pouvez utiliser AbortController pour annuler une demande de récupération.
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 && <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>
la source
Depuis que le message a été ouvert, un "abortable-fetch" a été ajouté. https://developers.google.com/web/updates/2017/09/abortable-fetch
(à partir de la documentation :)
Le contrôleur + la manœuvre de signal 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 le standard DOM, et c'est l'API entière. Il est délibérément générique et peut donc être utilisé par d'autres standards Web et bibliothèques JavaScript.
par exemple, voici comment définir 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); });
la source
Le nœud de cet avertissement est que votre composant a une référence à lui qui est détenue par un rappel / promesse exceptionnelle.
Pour éviter l'anti-modèle de garder votre état isMounted autour (qui maintient votre composant en vie) comme cela a été fait dans le deuxième modèle, le site Web react suggère d' utiliser une promesse facultative ; cependant, ce code semble également maintenir 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 (dactylographié)…
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. } }
la source
this
Lorsque j'ai besoin "d'annuler tous les abonnements et de manière asynchrone", j'envoie généralement quelque chose à reduxer dans componentWillUnmount pour informer tous les autres abonnés et envoyer une autre demande d'annulation au serveur si nécessaire
la source
Je pense que s'il n'est pas nécessaire d'informer le serveur de l'annulation, la meilleure approche consiste simplement à utiliser la syntaxe async / await (si elle est disponible).
constructor(props){ super(props); this.state = { isLoading: true, dataSource: [{ name: 'loading...', id: 'loading', }] } } async componentDidMount() { try { const responseJson = await fetch('LINK HERE') .then((response) => response.json()); this.setState({ isLoading: false, dataSource: responseJson, } } catch { console.error(error); } }
la source
En plus des exemples de hooks de promesse annulable dans la solution acceptée, il peut être pratique d'avoir un
useAsyncCallback
hook encapsulant un rappel de demande et renvoyant une promesse annulable. L'idée est la même, mais avec un crochet fonctionnant comme un habituéuseCallback
. Voici un exemple de mise en œuvre:function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) { const isMounted = useRef(true) useEffect(() => { return () => { isMounted.current = false } }, []) const cb = useCallback(callback, dependencies) const cancellableCallback = useCallback( (...args: any[]) => new Promise<T>((resolve, reject) => { cb(...args).then( value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })), error => (isMounted.current ? reject(error) : reject({ isCanceled: true })) ) }), [cb] ) return cancellableCallback }
la source
Je pense avoir trouvé un moyen de contourner cela. Le problème n'est pas tant la récupération elle-même que le setState après le rejet du composant. La solution était donc de définir
this.state.isMounted
commefalse
et puis de lecomponentWillMount
changer sur true, et decomponentWillUnmount
définir à nouveau sur false. Ensuite, justeif(this.state.isMounted)
le setState à l'intérieur de la récupération. Ainsi:constructor(props){ super(props); this.state = { isMounted: false, isLoading: true, dataSource: [{ name: 'loading...', id: 'loading', }] } } componentDidMount(){ this.setState({ isMounted: true, }) return fetch('LINK HERE') .then((response) => response.json()) .then((responseJson) => { if(this.state.isMounted){ this.setState({ isLoading: false, dataSource: responseJson, }, function(){ }); } }) .catch((error) =>{ console.error(error); }); } componentWillUnmount() { this.setState({ isMounted: false, }) }
la source