Acho que o título diz tudo. O aviso amarelo é exibido sempre que eu desmonto um componente que ainda está em busca.
ConsoleAviso: Não é possível chamar
setState
(ouforceUpdate
) em um componente não montado. Este é um ambiente autônomo, mas ... Para corrigir, cancele todas as assinaturas e tarefas assíncronas nocomponentWillUnmount
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);
});
}
Respostas:
Quando você dispara uma promessa, pode levar alguns segundos antes que ela seja resolvida e, nessa altura, o usuário pode ter navegado para outro local em seu aplicativo. Portanto, quando o Promise resolve
setState
é executado em um componente não montado e você obtém um erro - como no seu caso. Isso também pode causar vazamentos de memória.É por isso que é melhor remover parte de sua lógica assíncrona dos componentes.
Caso contrário, você precisará cancelar sua promessa de alguma forma . Alternativamente - como técnica de último recurso (é um antipadrão) - você pode manter uma variável para verificar se o componente ainda está montado:
componentDidMount(){ this.mounted = true; this.props.fetchData().then((response) => { if(this.mounted) { this.setState({ data: response }) } }) } componentWillUnmount(){ this.mounted = false; }
Vou enfatizar isso novamente - este é um antipadrão, mas pode ser suficiente no seu caso (assim como fizeram com a
Formik
implementação).Uma discussão semelhante no GitHub
EDITAR:
Provavelmente é assim que eu resolveria o mesmo problema (não tendo nada além de React) com Ganchos :
OPÇÃO 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; }
OPÇÃO B: Alternativamente, com o
useRef
qual se comporta como uma propriedade estática de uma classe, o que significa que não faz o componente rerender quando seu valor muda: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 }
Exemplo: https://codesandbox.io/s/86n1wq2z8
fonte
O pessoal amigável do React recomenda envolver suas chamadas / promessas de busca em uma promessa cancelável. Embora não haja nenhuma recomendação nessa documentação para manter o código separado da classe ou função com a busca, isso parece aconselhável porque outras classes e funções provavelmente precisarão dessa funcionalidade, a duplicação de código é um antipadrão e independentemente do código remanescente deve ser descartado ou cancelado em
componentWillUnmount()
. De acordo com React, você pode chamarcancel()
a promessa encapsulada emcomponentWillUnmount
para evitar definir o estado em um componente não montado.O código fornecido seria semelhante a estes snippets de código se usarmos o React como um guia:
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 ----
Eu descobri que a resposta dada pode não estar totalmente correta, acompanhando o problema no GitHub. Aqui está uma versão que uso e que funciona para meus objetivos:
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; } }; };
A ideia era ajudar o coletor de lixo a liberar memória tornando a função ou qualquer outra coisa nula.
fonte
Você pode usar AbortController para cancelar uma solicitação de busca.
Veja também: 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>
fonte
Since the post had been opened, an "abortable-fetch" has been added. https://developers.google.com/web/updates/2017/09/abortable-fetch
(from the docs:)
The controller + signal manoeuvre Meet the AbortController and AbortSignal:
const controller = new AbortController(); const signal = controller.signal;
The controller only has one method:
controller.abort(); When you do this, it notifies the signal:
signal.addEventListener('abort', () => { // Logs true: console.log(signal.aborted); });
This API is provided by the DOM standard, and that's the entire API. It's deliberately generic so it can be used by other web standards and JavaScript libraries.
for example, here's how you'd make a fetch timeout after 5 seconds:
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); });
fonte
The crux of this warning is that your component has a reference to it that is held by some outstanding callback/promise.
To avoid the antipattern of keeping your isMounted state around (which keeps your component alive) as was done in the second pattern, the react website suggests using an optional promise; however that code also appears to keep your object alive.
Instead, I've done it by using a closure with a nested bound function to setState.
Here's my constructor(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. } }
fonte
this
When I need to "cancel all subscriptions and asynchronous" I usually dispatch something to redux in componentWillUnmount to inform all other subscribers and send one more request about cancellation to server if necessary
fonte
I think if it is not necessary to inform server about cancellation - best approach is just to use async/await syntax (if it is available).
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); } }
fonte
In addition to the cancellable promise hooks examples in the accepted solution, it can be handy to have a
useAsyncCallback
hook wrapping a request callback and returning a cancellable promise. The idea is the same, but with a hook working just like a regularuseCallback
. Here is an example of implementation: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 }
fonte
I think I figured a way around it. The problem is not as much the fetching itself but the setState after the component is dismissed. So the solution was to set
this.state.isMounted
asfalse
and then oncomponentWillMount
change it to true, and incomponentWillUnmount
set to false again. Then justif(this.state.isMounted)
the setState inside the fetch. Like so: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, }) }
fonte