Creo que el título lo dice todo. La advertencia amarilla se muestra cada vez que desmonto un componente que todavía se está recuperando.
ConsolaAdvertencia: No se puede llamar
setState
(oforceUpdate
) en un componente desmontado. Esta es una operación no operativa, pero ... Para solucionarlo, cancele todas las suscripciones y tareas asincrónicas en elcomponentWillUnmount
método.
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);
});
}
Respuestas:
Cuando dispara una Promise, pueden pasar unos segundos antes de que se resuelva y, en ese momento, el usuario podría haber navegado a otro lugar en su aplicación. Entonces, cuando Promise resuelve
setState
se ejecuta en un componente desmontado y obtiene un error, como en su caso. Esto también puede provocar pérdidas de memoria.Por eso es mejor sacar parte de su lógica asincrónica de los componentes.
De lo contrario, deberá cancelar de alguna manera su Promesa . Alternativamente, como técnica de último recurso (es un antipatrón), puede mantener una variable para verificar si el componente aún está montado:
componentDidMount(){ this.mounted = true; this.props.fetchData().then((response) => { if(this.mounted) { this.setState({ data: response }) } }) } componentWillUnmount(){ this.mounted = false; }
Lo enfatizaré nuevamente: este es un antipatrón, pero puede ser suficiente en su caso (al igual que lo hicieron con la
Formik
implementación).Una discusión similar en GitHub
EDITAR:
Probablemente así es como resolvería el mismo problema (sin tener nada más que Reaccionar) con Hooks :
OPCION 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; }
OPCIÓN B: Alternativamente con
useRef
que se comporta como una propiedad estática de una clase, lo que significa que no hace que el componente se vuelva a procesar cuando cambia su valor: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 }
Ejemplo: https://codesandbox.io/s/86n1wq2z8
fuente
La gente amable de React recomienda envolver sus llamadas / promesas de búsqueda en una promesa cancelable. Si bien no hay ninguna recomendación en esa documentación para mantener el código separado de la clase o función con la recuperación, esto parece aconsejable porque es probable que otras clases y funciones necesiten esta funcionalidad, la duplicación de código es un anti-patrón, e independientemente del código persistente debe eliminarse o cancelarse en
componentWillUnmount()
. Según React, puede invocarcancel()
la promesa envueltacomponentWillUnmount
para evitar establecer el estado en un componente desmontado.El código proporcionado se parecería a estos fragmentos de código si usamos React como guía:
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(); }
---- EDITAR ----
He descubierto que la respuesta dada puede no ser del todo correcta siguiendo el problema en GitHub. Aquí hay una versión que uso y que funciona para mis propósitos:
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; } }; };
La idea era ayudar al recolector de basura a liberar memoria haciendo que la función o lo que sea que uses sea nulo.
fuente
Puede usar AbortController para cancelar una solicitud de recuperación.
Véase también: 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>
fuente
Desde que se abrió la publicación, se agregó una "recuperación abortable". https://developers.google.com/web/updates/2017/09/abortable-fetch
(de los documentos :)
El controlador + maniobra de señal Conozca AbortController y AbortSignal:
const controller = new AbortController(); const signal = controller.signal;
El controlador solo tiene un método:
controlador.abort (); Cuando haces esto, notifica la señal:
signal.addEventListener('abort', () => { // Logs true: console.log(signal.aborted); });
Esta API es proporcionada por el estándar DOM, y esa es la API completa. Es deliberadamente genérico, por lo que puede ser utilizado por otros estándares web y bibliotecas de JavaScript.
por ejemplo, así es como haría un tiempo de espera de recuperación después de 5 segundos:
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); });
fuente
El quid de esta advertencia es que su componente tiene una referencia que se encuentra en alguna devolución de llamada / promesa pendiente.
Para evitar el antipatrón de mantener su estado isMounted (que mantiene vivo su componente) como se hizo en el segundo patrón, el sitio web react sugiere usar una promesa opcional ; sin embargo, ese código también parece mantener vivo su objeto.
En cambio, lo hice usando un cierre con una función enlazada anidada para setState.
Aquí está mi constructor (mecanografiado) ...
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. } }
fuente
this
Cuando necesito "cancelar todas las suscripciones y de forma asíncrona", normalmente envío algo a redux en componentWillUnmount para informar a todos los demás suscriptores y enviar una solicitud más sobre la cancelación al servidor si es necesario
fuente
Creo que si no es necesario informar al servidor sobre la cancelación, el mejor enfoque es usar la sintaxis async / await (si 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); } }
fuente
Además de los ejemplos de ganchos de promesa cancelables en la solución aceptada, puede ser útil tener un
useAsyncCallback
gancho que envuelva una devolución de llamada de solicitud y devuelva una promesa cancelable. La idea es la misma, pero con un gancho que funciona como un normaluseCallback
. A continuación, se muestra un ejemplo de implementación: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 }
fuente
Creo que encontré una forma de evitarlo. El problema no es tanto la búsqueda en sí, sino el setState después de que se descarta el componente. Entonces, la solución fue establecer
this.state.isMounted
comofalse
y luegocomponentWillMount
cambiarlo a verdadero, y luegocomponentWillUnmount
establecerlo a falso nuevamente. Luego, soloif(this.state.isMounted)
el setState dentro de la búsqueda. Al igual que: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, }) }
fuente