Я пишу приложение на React и не смог избежать очень распространенной ловушки - вызова setState(...) после componentWillUnmount(...).
Я очень внимательно посмотрел на свой код и попытался поставить некоторые защитные предложения, но проблема не исчезла, и я все еще наблюдаю за предупреждением.
Поэтому у меня два вопроса:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.
in TextLayerInternal (created by Context.Consumer)
in TextLayer (created by PageInternal) index.js:1446
d/console[e]
index.js:1446
warningWithoutStack
react-dom.development.js:520
warnAboutUpdateOnUnmounted
react-dom.development.js:18238
scheduleWork
react-dom.development.js:19684
enqueueSetState
react-dom.development.js:12936
./node_modules/react/cjs/react.development.js/Component.prototype.setState
react.development.js:356
_callee$
TextLayer.js:97
tryCatch
runtime.js:63
invoke
runtime.js:282
defineIteratorMethods/</prototype[method]
runtime.js:116
asyncGeneratorStep
asyncToGenerator.js:3
_throw
asyncToGenerator.js:29
Book.tsx
import { throttle } from 'lodash';
import * as React from 'react';
import { AutoWidthPdf } from '../shared/AutoWidthPdf';
import BookCommandPanel from '../shared/BookCommandPanel';
import BookTextPath from '../static/pdf/sde.pdf';
import './Book.css';
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: () => void;
pdfWrapper: HTMLDivElement | null = null;
isComponentMounted: boolean = false;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
constructor(props: any) {
super(props);
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
);
}
componentDidMount = () => {
this.isComponentMounted = true;
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
this.isComponentMounted = false;
window.removeEventListener("resize", this.setDivSizeThrottleable);
};
render = () => (
<div className = "Book">
{ this.state.hidden && <div className = "Book__LoadNotification centered">Book is being loaded...</div> }
<div className = {this.getPdfContentContainerClassName()}>
<BookCommandPanel
bookTextPath = {BookTextPath}
/>
<div className = "Book__PdfContent" ref = {ref => this.pdfWrapper = ref}>
<AutoWidthPdf
file = {BookTextPath}
width = {this.state.pdfWidth}
onLoadSuccess = {(_: any) => this.onDocumentComplete()}
/>
</div>
<BookCommandPanel
bookTextPath = {BookTextPath}
/>
</div>
</div>
);
getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';
onDocumentComplete = () => {
try {
this.setState({ hidden: false });
this.setDivSizeThrottleable();
} catch (caughtError) {
console.warn({ caughtError });
}
};
}
export default Book;
AutoWidthPdf.tsx
import * as React from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
interface IProps {
file: string;
width: number;
onLoadSuccess: (pdf: any) => void;
}
export class AutoWidthPdf extends React.Component<IProps> {
render = () => (
<Document
file = {this.props.file}
onLoadSuccess = {(_: any) => this.props.onLoadSuccess(_)}
>
<Page
pageNumber = {1}
width = {this.props.width}
/>
</Document>
);
}
const DEFAULT_WIDTH = 140;
class Book extends React.Component {
setDivSizeThrottleable: ((() => void) & Cancelable) | undefined;
pdfWrapper: HTMLDivElement | null = null;
state = {
hidden: true,
pdfWidth: DEFAULT_WIDTH,
};
componentDidMount = () => {
this.setDivSizeThrottleable = throttle(
() => {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
},
500,
);
this.setDivSizeThrottleable();
window.addEventListener("resize", this.setDivSizeThrottleable);
};
componentWillUnmount = () => {
window.removeEventListener("resize", this.setDivSizeThrottleable!);
this.setDivSizeThrottleable!.cancel();
this.setDivSizeThrottleable = undefined;
};
render = () => (
<div className = "Book">
{ this.state.hidden && <div className = "Book__LoadNotification centered">Book is being loaded...</div> }
<div className = {this.getPdfContentContainerClassName()}>
<BookCommandPanel
BookTextPath = {BookTextPath}
/>
<div className = "Book__PdfContent" ref = {ref => this.pdfWrapper = ref}>
<AutoWidthPdf
file = {BookTextPath}
width = {this.state.pdfWidth}
onLoadSuccess = {(_: any) => this.onDocumentComplete()}
/>
</div>
<BookCommandPanel
BookTextPath = {BookTextPath}
/>
</div>
</div>
);
getPdfContentContainerClassName = () => this.state.hidden ? 'hidden' : '';
onDocumentComplete = () => {
try {
this.setState({ hidden: false });
this.setDivSizeThrottleable!();
} catch (caughtError) {
console.warn({ caughtError });
}
};
}
export default Book;
@ ic3b3rg проблема исчезает, если нет кода прослушивания событий
ок, а предложение сделать this.setDivSizeThrottleable.cancel() вместо гвардии this.isComponentMounted пробовали?
@ ic3b3rg То же предупреждение во время выполнения.
Возможный дубликат React - setState () на размонтированном компоненте



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Обновлено: я только что понял, что предупреждение относится к компоненту TextLayerInternal. Вероятно, это ваша ошибка. Остальное по-прежнему актуально, но, возможно, это не решит вашу проблему.
1) Получить экземпляр компонента для этого предупреждения сложно. Похоже, что есть некоторые обсуждения по улучшению этого в React, но в настоящее время нет простого способа сделать это. Я подозреваю, что причина, по которой он еще не был построен, вероятно, заключается в том, что компоненты должны быть написаны таким образом, что setState после размонтирования невозможно, независимо от состояния компонента. Проблема, с точки зрения команды React, всегда заключается в коде компонента, а не в экземпляре компонента, поэтому вы получаете имя типа компонента.
Этот ответ может быть неудовлетворительным, но я думаю, что смогу решить вашу проблему.
2) Регулируемая функция Lodashes имеет метод cancel. Вызовите cancel в componentWillUnmount и откажитесь от isComponentMounted. Отмена - это более «идиоматическая» реакция, чем введение нового свойства.
Проблема в том, что я не контролирую TextLayerInternal напрямую. Таким образом, я не знаю, «кто виноват в звонке setState()». Я попробую cancel по вашему совету и посмотрю, как он пойдет,
К сожалению, я все еще вижу предупреждение. Пожалуйста, проверьте код в разделе «Обновление 1», чтобы убедиться, что я все делаю правильно.
попробуйте заменить setDivSizeThrottleable на
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
{ leading: false, trailing: true }
);
Я пробовал. Теперь я постоянно вижу предупреждение, которое я наблюдал только время от времени, при изменении размера окна перед внесением этого изменения. ¯_ (ツ) _ / ¯ Тем не менее, спасибо за попытку.
To remove - Can't perform a React state update on an unmounted component warning, use componentDidMount method under a condition and make false that condition on componentWillUnmount method. For example : -
class Home extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true;
ajaxVar
.get('https://domain')
.then(result => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
Это сработало, но почему это должно работать? Что именно вызывает эту ошибку? и как это исправить: |
Работает нормально. Он останавливает повторяющийся вызов метода setState, поскольку он проверяет значение _isMounted перед вызовом setState, а затем, наконец, снова сбрасывает значение false в componentWillUnmount (). Я думаю, так оно и работает.
для компонента крючка используйте это: const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
@ x-magix Для этого вам действительно не нужна ссылка, вы можете просто использовать локальную переменную, которую может закрыть функция возврата.
@Abhinav Я думаю, почему это работает, так это то, что _isMounted не управляется React (в отличие от state) и, следовательно, не подчиняется конвейер рендеринга React. Проблема в том, что когда компонент настроен на размонтирование, React удаляет из очереди все вызовы setState() (что может вызвать «повторную визуализацию»); поэтому состояние никогда не обновляется
Если вы используете Babel и включили экспериментальные предложения JavaScript, вы можете использовать поле частного класса для флага, то есть this.#isMounted
Фантастика! У меня сработало нормально.
@Mordechai Я считаю, что это было сделано, чтобы избежать следующего предупреждения компилятора: `` Назначения переменной isMounted изнутри React Hook useEffect будут потеряны после каждого рендеринга. Чтобы сохранить значение с течением времени, сохраните его в хуке useRef и сохраните изменяемое значение в свойстве .current. В противном случае вы можете переместить эту переменную прямо внутрь useEffect `` ''
Вы не получите это предупреждение о том, что переменная объявлена локально в useEffect и пустом массиве зависимостей.
простое для понимания решение является большим преимуществом по сравнению со сложным для понимания js в некоторых других решениях, плюс ответ показан в контексте компонента. отличный ответ.
Я знаю, что вы не используете историю, но в моем случае я использовал ловушку useHistory из модели React Router DOM, которая отключает компонент до того, как состояние сохраняется в моем провайдере контекста React.
Чтобы решить эту проблему, я использовал ловушку withRouter, встраивающую компонент, в моем случае export default withRouter(Login), и внутри компонента const Login = props => { ...; props.history.push("/dashboard"); .... Я также удалил другой props.history.push из компонента, например if (authorization.token) return props.history.push('/dashboard'), потому что это вызывает цикл из-за состояния authorization.
Альтернатива помещению нового элемента в история.
Вот конкретное решение Реагировать на хуки для
Warning: Can't perform a React state update on an unmounted component.
Вы можете объявить let isMounted = true внутри useEffect, который будет изменен в обратный вызов очистки, как только компонент будет размонтирован. Перед обновлением состояния вы теперь проверяете эту переменную условно:
useEffect(() => {
let isMounted = true; // note mutable flag
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []); // adjust dependencies to your needs
const Parent = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
Parent:
<button onClick = {() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"} Child
</button>
{mounted && <Child />}
<p>
Unmount Child, while it is still loading. It won't set state later on,
so no error is triggered.
</p>
</div>
);
};
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useEffect(() => {
let isMounted = true;
fetchData();
return () => {
isMounted = false;
};
// simulate some Web API fetching
function fetchData() {
setTimeout(() => {
// drop "if (isMounted)" to trigger error again
// (take IDE, doesn't work with stack snippet)
if (isMounted) setState("data fetched")
else console.info("aborted setState on unmounted component")
}, 4000);
}
}, []);
return <div>Child: {state}</div>;
};
ReactDOM.render(<Parent />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>useAsync HookМы можем инкапсулировать весь шаблон в настраиваемый Hook, который автоматически прерывает асинхронные функции в случае, если компонент отключен или значения зависимостей изменились ранее:
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data);
});
return () => { isActive = false };
}, [asyncFn, onSuccess]);
}
// custom Hook for automatic abortion on unmount or dependency change
// You might add onFailure for promise errors as well.
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data)
else console.info("aborted setState on unmounted component")
});
return () => {
isActive = false;
};
}, [asyncFn, onSuccess]);
}
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useAsync(simulateFetchData, setState);
return <div>Child: {state}</div>;
};
const Parent = () => {
const [mounted, setMounted] = useState(true);
return (
<div>
Parent:
<button onClick = {() => setMounted(!mounted)}>
{mounted ? "Unmount" : "Mount"} Child
</button>
{mounted && <Child />}
<p>
Unmount Child, while it is still loading. It won't set state later on,
so no error is triggered.
</p>
</div>
);
};
const simulateFetchData = () => new Promise(
resolve => setTimeout(() => resolve("data fetched"), 4000));
ReactDOM.render(<Parent />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>Подробнее об очистке эффектов: Чрезмерная реакция: полное руководство по использованию эффекта
твои уловки работают! Интересно, в чем заключается магия?
Здесь мы используем встроенный эффект уборка, который запускается при изменении зависимостей и в любом случае при отключении компонента. Так что это идеальное место для переключения флага isMounted на false, доступ к которому можно получить из области закрытия обратного вызова окружающего эффекта. Вы можете думать о функции очистки как о принадлежащий к ее соответствующем эффекте.
это имеет смысл! Я доволен твоим ответом. Я извлек из этого урок.
святой курит ... эта штука isMounted работает. Я использую response-testing-lib 10.4.7, а на formik ^2.1.4. Это похоже на полный взлом и результат чего-то с Formik.
@PhilLucks рад, что это работает. Этот шаблон - очень распространенный и простой способ прервать асинхронные функции, без взлома (если я не ошибаюсь, аналогичный подход даже рекомендуется в FAQ по хукам).
Итак, должны ли мы обернуть все наши обновления состояния в if (isMounted) {/ * здесь обновление состояния * /} ??
@VictorMolina Нет, это определенно было бы излишним. Рассмотрим этот метод для компонентов a), использующих асинхронные операции, такие как fetch в useEffect и b), которые нестабильны, т.е. могут быть размонтированы до того, как будет возвращен результат async, и готовы к установке в качестве состояния.
Ой! Добавив await в мои функции async, он работает так, как ожидалось!
@AlexisWilke: да, вы можете определить внутреннюю функцию async внутри тела обратного вызова useEffectкак показано в этом посте. Таким образом, вы можете использовать await вместо myPromise.then(...).
@Goran_Ilic_Ilke, мы этого не делаем. return () => { isMounted = false }; означает: вернуть нулевой параметр стрелочная функция, тип возврата которого - void, поскольку внутри мы применяем побочный эффект переключения переменной состояния isMounted. Я думаю, вы перепутали его с return () => ({ isMounted = false });, см. Ссылку выше.
Это кажется хорошим решением, если вы уже сузили круг причин / useEffect, вызывающих проблему, но в одном из вопросов 1. спрашивается, как определить, какой компонент, обработчик, перехватчик и т. д. Несет ответственность за эту ошибку. Почему так много положительных отзывов о проблеме, в которых не упоминается вся проблема в целом и не даются ответы на оба вопроса? Я проверил, и во всей этой ветке также нет сообщений о каких-либо советах по поиску источника ошибки; по трассировке стека невозможно выяснить?
Обратите внимание, что пользовательский хук useAsync будет срабатывать при каждом рендеринге, если вы не оберните обратные вызовы в useCallback.
@Woodz да, хороший намек. useCallback - это обычный и рекомендуемый способ в React возложить ответственность за зависимости на клиента useAsync. Вы можете переключиться на изменяемые ссылки внутри useAsync для хранения самого последнего обратного вызова, чтобы клиенты могли напрямую передавать свои функции / обратные вызовы без зависимостей. Но я бы экономно использовал этот шаблон, поскольку, вероятно, это более запутанный и императивный подход.
Вопрос, если я просто проигнорирую это, может ли это повредить моему приложению? Я пока не видел, чтобы это повредило или испортило мое приложение.
Если вышеуказанные решения не работают, попробуйте это, и это сработает для меня:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
@BadriPaudel возвращает null при экранировании компонента, он больше не будет хранить какие-либо данные в памяти
что вернуть? просто наклеить как есть?
Вы сэкономили мне время. Большое спасибо. без него не смог бы пройти тест React.
Я не рекомендую это решение, оно довольно хакерское. @BadriPaudel Это заменит функцию setState после componentWillUnmount функцией, которая ничего не делает. Функция setState будет по-прежнему вызываться.
У меня была аналогичная проблема и я ее решил:
Я автоматически вводил пользователя в систему, отправляя действие на redux (размещение токена аутентификации в состоянии redux)
а затем я пытался показать сообщение с this.setState ({succ_message: "...") в моем компоненте.
Компонент выглядел пустым с той же ошибкой на консоли: «отключенный компонент» .. «утечка памяти» и т. д.
После того, как я прочитал ответ Уолтера в этой теме
Я заметил, что в таблице маршрутизации моего приложения маршрут моего компонента недействителен, если пользователь вошел в систему:
{!this.props.user.token &&
<div>
<Route path = "/register/:type" exact component = {MyComp} />
</div>
}
Я сделал Маршрут видимым независимо от того, существует токен или нет.
У меня была аналогичная проблема, спасибо, что @ ford04 мне помог.
Однако произошла другая ошибка.
NB. Я использую перехватчики ReactJS
ndex.js:1 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
What causes the error?
import {useHistory} from 'react-router-dom'
const History = useHistory()
if (true) {
history.push('/new-route');
}
return (
<>
<render component />
</>
)
Это не могло сработать, потому что, несмотря на то, что вы перенаправляете на новую страницу, все состояние и свойства обрабатываются в dom или просто рендеринг на предыдущую страницу не останавливается.
What solution I found
import {Redirect} from 'react-router-dom'
if (true) {
return <redirect to = "/new-route" />
}
return (
<>
<render component />
</>
)
Хорошее решение, но оно должно быть возвращено <Redirect to = "/ new-route" />
Основываясь на ответе @ ford04, вот то же самое, заключенное в методе:
import React, { FC, useState, useEffect, DependencyList } from 'react';
export function useEffectAsync( effectAsyncFun : ( isMounted: () => boolean ) => unknown, deps?: DependencyList ) {
useEffect( () => {
let isMounted = true;
const _unused = effectAsyncFun( () => isMounted );
return () => { isMounted = false; };
}, deps );
}
Использование:
const MyComponent : FC<{}> = (props) => {
const [ asyncProp , setAsyncProp ] = useState( '' ) ;
useEffectAsync( async ( isMounted ) =>
{
const someAsyncProp = await ... ;
if ( isMounted() )
setAsyncProp( someAsyncProp ) ;
});
return <div> ... ;
} ;
Если вы извлекаете данные из axios, а ошибка все еще возникает, просто заключите сеттер в условие
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.info(err));
return () => {
isRendered = false;
};
}, []);
почему вы не используете isRendered без .current? это особенность?
@usertest Я только что реализовал это решение, но мне пришлось использовать .current.
В зависимости от того, как вы открываете свою веб-страницу, вы можете не вызывать монтирование. Например, использование <Link/> для возврата к странице, которая уже была смонтирована в виртуальной модели DOM, поэтому перехватывается требование данных из жизненного цикла componentDidMount.
Вы хотите сказать, что componentDidMount() можно вызывать дважды без промежуточного вызова componentWillUnmount() между ними? Я не думаю, что это возможно.
Нет, я говорю, что он не вызывается дважды, поэтому страница не обрабатывает код внутри componentDidMount() при использовании <Link/>. Я использую Redux для решения этих проблем и храню данные веб-страницы в хранилище Reducer, так что мне все равно не нужно перезагружать страницу.
Существует довольно распространенная ловушка под названием useIsMounted, которая решает эту проблему (для функциональных компонентов) ...
import { useRef, useEffect } from 'react';
export function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, []);
return isMounted;
}
затем в вашем функциональном компоненте
function Book() {
const isMounted = useIsMounted();
...
useEffect(() => {
asyncOperation().then(data => {
if (isMounted.current) { setState(data); }
})
});
...
}
Можем ли мы использовать один и тот же крючок с несколькими компонентами?
@AyushKumar: да, можно! в этом вся прелесть крючков! состояние isMounted будет специфическим для каждого компонента, который вызывает useIsMounted!
Думаю, этот способ решения useIsMounted должен быть включен в основной пакет.
Другой вопрос: добавил ли я UseIsMounted в свой хук useEffect и запустил ли я слушатель. Приведет ли добавление return () => в код к какой-либо утечке?
Вдохновленный принятым ответом @ ford04, у меня был даже лучший подход к его решению, вместо того, чтобы использовать useEffect внутри useAsync, создать новую функцию, которая возвращает обратный вызов для componentWillUnmount:
function asyncRequest(asyncRequest, onSuccess, onError, onComplete) {
let isMounted=true
asyncRequest().then((data => isMounted ? onSuccess(data):null)).catch(onError).finally(onComplete)
return () => {isMounted=false}
}
...
useEffect(()=>{
return asyncRequest(()=>someAsyncTask(arg), response=> {
setSomeState(response)
},onError, onComplete)
},[])
Я бы не рекомендовал полагаться на локальную переменную isMounted, но вместо этого сделайте ее состоянием (через ловушку useState).
Какое это имеет значение? По крайней мере, я не могу придумать другого поведения.
Вдохновленный ответом @ ford04, я использую этот хук, который также принимает обратные вызовы для успеха, ошибок и, наконец, abortFn:
export const useAsync = (
asyncFn,
onSuccess = false,
onError = false,
onFinally = false,
abortFn = false
) => {
useEffect(() => {
let isMounted = true;
const run = async () => {
try{
let data = await asyncFn()
if (isMounted && onSuccess) onSuccess(data)
} catch(error) {
if (isMounted && onError) onSuccess(error)
} finally {
if (isMounted && onFinally) onFinally()
}
}
run()
return () => {
if (abortFn) abortFn()
isMounted = false
};
}, [asyncFn, onSuccess])
}
Если asyncFn выполняет какую-то выборку из серверной части, часто имеет смысл прервать ее, когда компонент отключен (хотя не всегда, иногда, например, если вы загружаете некоторые данные в хранилище, вы также можете просто захотеть доделать, даже если компонент размонтирован)
Подход isMounted в большинстве случаев является анти-шаблоном, потому что он на самом деле ничего не очищает / не отменяет, он просто избегает изменения состояния на несмонтированных компонентах, но ничего не делает с ожидающими асинхронными задачами. Команда React недавно удален предупреждает об утечке, потому что пользователи продолжают создавать множество антишаблонов, чтобы скрыть предупреждение, а не исправить его причину.
Но написать отменяемый код на простом JS может быть очень сложно. Чтобы исправить это, я создал свою собственную библиотеку useAsyncEffect2 с настраиваемыми хуками, построенную на основе отменяемого обещания (c-обещание2) для выполнения отменяемого асинхронного кода для достижения его постепенной отмены. Все асинхронные этапы (обещания), в том числе глубокие, отменяются. Это означает, что запрос здесь будет автоматически прерван, если его родительский контекст будет отменен. Конечно, вместо запроса можно использовать любую другую асинхронную операцию.
useAsyncEffect с простым использованием useState (Живая демонстрация): import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent({url}) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
setText("fetching...");
const json = (yield cpAxios(url)).data;
setText(`Success: ${JSON.stringify(json)}`);
},
[url]
);
return (
<div>
<div>{text}</div>
<button onClick = {cancel}>
Cancel request
</button>
</div>
);
}
useAsyncEffect Демо с использованием внутренних состояний (Живая демонстрация): import React from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpAxios from "cp-axios";
function TestComponent({ url, timeout }) {
const [cancel, done, result, err] = useAsyncEffect(
function* () {
return (yield cpAxios(url).timeout(timeout)).data;
},
{ states: true, deps: [url] }
);
return (
<div>
{done ? (err ? err.toString() : JSON.stringify(result)) : "loading..."}
<button onClick = {cancel} disabled = {done}>
Cancel async effect (abort request)
</button>
</div>
);
}
import React, { Component } from "react";
import { ReactComponent } from "c-promise2";
import cpAxios from "cp-axios";
@ReactComponent
class TestComponent extends Component {
state = {
text: ""
};
*componentDidMount(scope) {
const { url, timeout } = this.props;
const response = yield cpAxios(url).timeout(timeout);
this.setState({ text: JSON.stringify(response.data, null, 2) });
}
render() {
return (<div>{this.state.text}</div>);
}
}
export default TestComponent;
Еще другие примеры:
const handleClick = async (item: NavheadersType, index: number) => {
const newNavHeaders = [...navheaders];
if (item.url) {
await router.push(item.url); =>>>> line causing error (causing route to happen)
// router.push(item.url); =>>> coreect line
newNavHeaders.forEach((item) => (item.active = false));
newNavHeaders[index].active = true;
setnavheaders([...newNavHeaders]);
}
};
Не могли бы вы добавить определение?
Решение от @ ford04 не сработало для меня, и особенно, если вам нужно использовать isMounted в нескольких местах (например, множественное использованиеEffect), рекомендуется использоватьRef, как показано ниже:
"dependencies":
{
"react": "17.0.1",
}
"devDependencies": {
"typescript": "4.1.5",
}
export const SubscriptionsView: React.FC = () => {
const [data, setData] = useState<Subscription[]>();
const isMounted = React.useRef(true);
React.useEffect(() => {
if (isMounted.current) {
// fetch data
// setData (fetch result)
return () => {
isMounted.current = false;
};
}
}
});
Согласитесь с точкой зрения, и это решение будет удобнее, и оно обеспечит единый источник истины.
Самое простое и компактное решение (с пояснением) рассматривается ниже как однострочное решение.
useEffect(() => { return () => {}; }, []);
Приведенный выше пример useEffect() возвращает функцию обратного вызова, запускающую React, чтобы завершить отключенную часть своего жизненного цикла до обновления состояния.
Это очень упрощенное решение - все, что нужно. Кроме того, он также работает в отличие от вымышленного синтаксиса, предоставляемого @ ford04 и @sfletche. Между прочим, приведенный ниже фрагмент кода из @ ford04 является чисто воображаемым синтаксисом (@sfletche, @vinod, @guneetgstar и @ Дрю Кордано использовали тот же самый воображаемый синтаксис).
data => { <--- Вымышленный / воображаемый синтаксис
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
Все мои линтеры и все линтеры моей команды не принимают его и сообщают о Uncaught SyntaxError: unexpected token: '=>'. Я удивлен, что никто не уловил воображаемый синтаксис. Может ли кто-нибудь, кто участвовал в этой цепочке вопросов, особенно среди проголосовавших, объяснить мне, как они заставили решения работать для них?
Ваше утверждение о «воображаемом синтаксисе» ошибочно. Это работает для многих других людей! Если ваш тип возвращаемого значения someAsynchronousOperation() - Promise<void>, то data обязательно вызовет ошибку компиляции TypeScript. Однако, если это Promise<X>, где X не является undefined / void / never, вы определенно сможете использовать .then(data => {...})! Вы не привели полного минимального примера, чтобы рассуждать об этом. Если вы хотите решить конкретную проблему с кодом, откройте отдельный вопрос на StackOverflow. Вы же не хотите получать отрицательные голоса или отмечать ответ.
Вы про стрелочную функцию? Это было введено в ES6. Я не знаю, как вы настроили линтер, но это очень распространенный синтаксис. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Ну, я не голосовал против вас ... Не зная ничего о ваших настройках линтера или чего-либо о вашем техническом стеке, я не могу точно сказать, почему это выглядит для вас как синтаксическая ошибка, но, возможно, это потому, что ваш линтер использует настройки для более старых версий Javascript, которые не поддерживают это, но это только предположение.
ОБНОВЛЕНИЕ НЕ ИСПОЛЬЗУЙТЕ МОЙ ОРИГИНАЛЬНЫЙ ОТВЕТ, ЭТО НЕ РАБОТАЕТ
Этот ответ был основан на использовании отменяемых обещаний и примечания в makecancelable, которое я перенес на использование хуков. Однако, похоже, это не отменяет цепочку async / await и даже cancelable-promiseне поддерживает отмену цепочки ожиданий.
Проведя немного дополнительных исследований, выяснилось, что некоторые внутренние причины Google препятствовали тому, чтобы отменяемые обещания вошли в стандарт.
Более того, было некоторое обещание с Bluebird, которое вводит отменяемые обещания, но оно не работает в Expo, или, по крайней мере, я не видел примера его работы в Expo.
принятый ответ - лучший. Поскольку я использую TypeScript, я адаптировал код с несколькими модификациями (я явно установил зависимости, поскольку неявные зависимости принятого ответа, по-видимому, дают цикл повторного рендеринга в моем приложении, добавлены и используют async / await вместо цепочки обещаний, передайте ref на смонтированный объект, чтобы цепочку async / await можно было отменить раньше, если это необходимо)
/**
* This starts an async function and executes another function that performs
* React state changes if the component is still mounted after the async
* operation completes
* @template T
* @param {(mountedRef: React.MutableRefObject<boolean>) => Promise<T>} asyncFunction async function,
* it has a copy of the mounted ref so an await chain can be canceled earlier.
* @param {(asyncResult: T) => void} onSuccess this gets executed after async
* function is resolved and the component is still mounted
* @param {import("react").DependencyList} deps
*/
export function useAsyncSetEffect(asyncFunction, onSuccess, deps) {
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
(async () => {
const x = await asyncFunction(mountedRef);
if (mountedRef.current) {
onSuccess(x);
}
})();
return () => {
mountedRef.current = false;
};
}, deps);
}
Поскольку у меня много разных операций с async, я использую пакет cancelable-promise, чтобы решить эту проблему с минимальными изменениями кода.
Предыдущий код:
useEffect(() =>
(async () => {
const bar = await fooAsync();
setSomeState(bar);
})(),
[]
);
Новый код:
import { cancelable } from "cancelable-promise";
...
useEffect(
() => {
const cancelablePromise = cancelable(async () => {
const bar = await fooAsync();
setSomeState(bar);
})
return () => cancelablePromise.cancel();
},
[]
);
Вы также можете включить его в пользовательскую служебную функцию, подобную этой
/**
* This wraps an async function in a cancelable promise
* @param {() => PromiseLike<void>} asyncFunction
* @param {React.DependencyList} deps
*/
export function useCancelableEffect(asyncFunction, deps) {
useEffect(() => {
const cancelablePromise = cancelable(asyncFunction());
return () => cancelablePromise.cancel();
}, deps);
}
Вот простое решение для этого. Это предупреждение возникает из-за того, что мы выполняем некоторый запрос на выборку, когда этот запрос находится в фоновом режиме (поскольку некоторые запросы занимают некоторое время), и мы возвращаемся с этого экрана, тогда они реагируют, не могут обновить состояние. вот пример кода для этого. написать эта строка перед каждым обновлением состояния.
if (!isScreenMounted.current) return;
Вот полный код
import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () => isScreenMounted.current = false
},[])
const ConvertFileSubmit = () => {
if (!isScreenMounted.current) return;
setUpLoading(true)
var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
formdata.append("file",file);
fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if (!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.info('error', error)
});
}
return(
<>
<StatusBar barStyle = "dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior = "automatic"
style = {styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}
export default SearchScreen;
const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})
Я решил эту проблему, предоставив все параметры, которые используются в хуке useEffect
Код сообщил об ошибке:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId]);
Код после исправления:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId,level]);
Как видите, проблемы решены после того, как я предоставил все параметры (включая параметр уровня), которые должны пройти.
Добавьте ссылку на компонент jsx, а затем проверьте, что он существует
function Book() {
const ref = useRef();
useEffect(() => {
asyncOperation().then(data => {
if (ref.current) setState(data);
})
});
return <div ref = {ref}>content</div>
}
Это работает для меня, приложение Nextjs
Проверка того, смонтирован ли компонент, на самом деле является анти-шаблоном согласно документации React. Решение для предупреждения setState заключается скорее в использовании AbortController.:
useEffect(() => {
const abortController = new AbortController() // creating an AbortController
fetch(url, { signal: abortController.signal }) // passing the signal to the query
.then(data => {
setState(data) // if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return // if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort() // stop the query by aborting on the AbortController on unmount
}
}, [])
Для асинхронных операций, которые не основаны на Fetch API, все же должен быть способ отменить эти асинхронные операции, и вам лучше использовать их, чем просто проверять, смонтирован ли компонент. Если вы создаете свой собственный API, вы можете реализовать в нем API AbortController для его обработки.
Для большего контекста, проверка того, установлен ли компонент, является анти-шаблоном как React внутренне проверяет, смонтирован ли компонент для отображения этого предупреждения.. Повторное выполнение той же проверки - просто способ скрыть предупреждение, и есть несколько более простых способов скрыть их, чем добавление этого фрагмента кода в большую часть кодовой базы.
Источник: https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934
React уже удалил это предупреждение но вот лучшее решение (не только обходной путь)
useEffect(() => {
const abortController = new AbortController() // creating an AbortController
fetch(url, { signal: abortController.signal }) // passing the signal to the query
.then(data => {
setState(data) // if everything went well, set the state
})
.catch(error => {
if (error.name === 'AbortError') return // if the query has been aborted, do nothing
throw error
})
return () => {
abortController.abort()
}
}, [])
"If there is a setter hook in your useEffect, return an cleanup function" - me
Проверьте свои функции useEffect(), у которых есть перехватчик установки setSomething(), и заставьте его возвращать функцию очистки в конце. Например:
useEffect(() => {
...
setSomething(something); //
...
return () => { }; // The cleanup function we have to add, it could be empty like this
}, [/*whatever deps or no deps at all*/]);
У меня есть 2 решения этой ошибки:
Если вы используете hook и useEffect, поставьте конец return на useEffect.
useEffect(() => {
window.addEventListener('mousemove', logMouseMove)
return () => {
window.removeEventListener('mousemove', logMouseMove)
}
}, [])
Если вы используете componentDidMount, поставьте рядом с ним componentWillUnmount.
componentDidMount() {
window.addEventListener('mousemove', this.logMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.logMouseMove)
}
Сохранится ли проблема, если вы закомментируете добавление и удаление слушателей?