Думаю, название говорит само за себя. Желтое предупреждение отображается каждый раз, когда я отключаю компонент, который все еще загружается.
ПриставкаПредупреждение: невозможно вызвать
setState
(илиforceUpdate
) отключенный компонент. Это не операция, но ... Чтобы исправить, отмените все подписки и асинхронные задачи в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);
});
}
Ответы:
Когда вы запускаете Promise, может пройти несколько секунд, прежде чем оно разрешится, и к тому времени пользователь, возможно, перейдет в другое место в вашем приложении. Поэтому, когда Promise resolves
setState
выполняется на несмонтированном компоненте, вы получаете сообщение об ошибке - как и в вашем случае. Это также может вызвать утечку памяти.Вот почему лучше всего вынести часть асинхронной логики из компонентов.
В противном случае вам нужно будет как-то отменить свое обещание . В качестве альтернативы - в крайнем случае (это антипаттерн) - вы можете сохранить переменную, чтобы проверять, смонтирован ли компонент:
componentDidMount(){ this.mounted = true; this.props.fetchData().then((response) => { if(this.mounted) { this.setState({ data: response }) } }) } componentWillUnmount(){ this.mounted = false; }
Подчеркну еще раз - это антипаттерн, но в вашем случае может быть достаточно (как и в случае с
Formik
реализацией).Аналогичное обсуждение на GitHub
РЕДАКТИРОВАТЬ:
Вероятно, вот как бы я решил ту же проблему (не имея ничего, кроме React) с помощью хуков :
ВАРИАНТ А:
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; }
ВАРИАНТ B: В качестве альтернативы
useRef
which ведет себя как статическое свойство класса, что означает, что он не выполняет повторную визуализацию компонента при изменении его значения: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 }
Пример: https://codesandbox.io/s/86n1wq2z8
источник
Дружелюбные люди в React рекомендуют заключать вызовы / обещания fetch в отменяемые обещания. Хотя в этой документации нет рекомендаций по хранению кода отдельно от класса или функции с выборкой, это кажется целесообразным, поскольку другие классы и функции могут нуждаться в этой функциональности, дублирование кода является анти-шаблоном и независимо от устаревшего кода. следует утилизировать или аннулировать в
componentWillUnmount()
. Согласно React, вы можете вызватьcancel()
обернутое обещание,componentWillUnmount
чтобы избежать установки состояния на отключенном компоненте.Предоставленный код будет выглядеть примерно так, если мы воспользуемся React в качестве руководства:
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(); }
---- РЕДАКТИРОВАТЬ ----
Я обнаружил, что данный ответ может быть не совсем правильным, следя за проблемой на GitHub. Вот одна из версий, которые я использую, и она подходит для моих целей:
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; } }; };
Идея заключалась в том, чтобы помочь сборщику мусора освободить память путем создания функции или того, что вы используете, null.
источник
Вы можете использовать AbortController, чтобы отменить запрос на выборку.
См. Также: 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>
источник
Так как пост был открыт, добавлена возможность прерывания. https://developers.google.com/web/updates/2017/09/abortable-fetch
(из документов :)
Контроллер + сигнальный маневр Встречайте AbortController и AbortSignal:
const controller = new AbortController(); const signal = controller.signal;
У контроллера есть только один метод:
controller.abort (); Когда вы это сделаете, он уведомит сигнал:
signal.addEventListener('abort', () => { // Logs true: console.log(signal.aborted); });
Этот API предоставляется стандартом DOM, и это весь API. Он намеренно общий, поэтому может использоваться другими веб-стандартами и библиотеками JavaScript.
например, вот как можно установить тайм-аут выборки через 5 секунд:
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); });
источник
Суть этого предупреждения в том, что у вашего компонента есть ссылка на него, удерживаемая каким-то невыполненным обратным вызовом / обещанием.
Чтобы избежать антипаттерна, заключающегося в поддержании вашего состояния isMounted (которое поддерживает работу вашего компонента), как это было сделано во втором шаблоне, сайт реакции предлагает использовать необязательное обещание ; однако этот код, похоже, также поддерживает ваш объект в живых.
Вместо этого я сделал это, используя закрытие с вложенной функцией привязки к setState.
Вот мой конструктор (машинописный текст)…
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. } }
источник
this
Когда мне нужно «отменить все подписки и асинхронно», я обычно отправляю что-то в redux в componentWillUnmount, чтобы проинформировать всех других подписчиков и при необходимости отправить еще один запрос об отмене на сервер.
источник
Я думаю, что если нет необходимости сообщать серверу об отмене - лучший подход - просто использовать синтаксис async / await (если он доступен).
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); } }
источник
В дополнение к примерам перехватов отменяемых обещаний в принятом решении может быть удобно иметь
useAsyncCallback
ловушку, обертывающую обратный вызов запроса и возвращающую отменяемое обещание. Идея та же, но с крючком, работающим как обычныйuseCallback
. Вот пример реализации: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 }
источник
Думаю, я придумал способ обойти это. Проблема не столько в самой выборке, сколько в setState после того, как компонент был отклонен. Итак, решение заключалось в том, чтобы установить
this.state.isMounted
как,false
а затемcomponentWillMount
изменить его на true иcomponentWillUnmount
снова установить на false. Затем простоif(this.state.isMounted)
setState внутри выборки. Вот так: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, }) }
источник