我正在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;
答案 0 :(得分:79)
这是针对
React Hooks 的特定解决方案
警告:无法在已卸载的组件上执行React状态更新。
您可以在let isMounted = true
内声明useEffect
,一旦卸载组件,它将在清理回调中进行更改。在状态更新之前,您现在有条件地检查此变量:
useEffect(() => {
let isMounted = true; // note this flag denote mount status
someAsyncOperation().then(data => {
if (isMounted) setState(data);
})
return () => { isMounted = false }; // use effect cleanup to set flag false, if unmounted
});
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; // note this mounted flag
fetchData();
return () => {
isMounted = false;
}; // use effect cleanup to set flag false, if unmounted
// simulate some Web API fetching
function fetchData() {
setTimeout(() => {
// drop "if (isMounted)" to trigger error again
if (isMounted) setState("data fetched");
}, 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中,该钩子知道在组件卸载之前如何处理并自动中止异步功能:
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isMounted = true;
asyncFn().then(data => {
if (isMounted) onSuccess(data);
});
return () => { isMounted = false };
}, [asyncFn, onSuccess]);
}
// use async operation with automatic abortion on unmount
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isMounted = true;
asyncFn().then(data => {
if (isMounted) onSuccess(data);
});
return () => {
isMounted = false;
};
}, [asyncFn, onSuccess]);
}
const Child = () => {
const [state, setState] = useState("loading (4 sec)...");
useAsync(delay, 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 delay = () => 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>
答案 1 :(得分:32)
如果上述解决方案不起作用,请尝试此操作,它对我有用:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
答案 2 :(得分:4)
我之所以发出此警告,可能是因为从效果挂钩中调用了<span class="emoji1"><i></i></span>
<span class="emoji2"><i></i></span>
(这在这3 issues linked together中进行了讨论)。
无论如何,升级react版本会删除警告。
答案 3 :(得分:4)
@ford04 的解决方案对我不起作用,特别是如果您需要在多个地方使用 isMounted(例如多个 useEffect),建议使用 useRef,如下所示:
"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;
};
}
}
});
答案 4 :(得分:3)
要删除-无法在未安装的组件警告上执行React状态更新,请在一定条件下使用componentDidMount方法,并在componentWillUnmount方法上将该条件设为false。例如:-
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() {
...
}
}
答案 5 :(得分:2)
有一个相当普遍的钩子称为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); }
})
});
...
}
答案 6 :(得分:2)
如果您正在从axios提取数据,但仍然出现错误,只需将setter包装在条件中
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);
答案 7 :(得分:1)
尝试将setDivSizeThrottleable
更改为
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
{ leading: false, trailing: true }
);
答案 8 :(得分:1)
由于我有许多不同的 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);
}
答案 9 :(得分:1)
由于@ 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.
什么原因导致错误?
import {useHistory} from 'react-router-dom'
const History = useHistory()
if (true) {
history.push('/new-route');
}
return (
<>
<render component />
</>
)
这不起作用,因为尽管您将重定向到新页面,但所有状态和道具都在dom上被操纵,或者只是呈现到上一页的操作并未停止。
我找到了什么解决办法
import {Redirect} from 'react-router-dom'
if (true) {
return <redirect to="/new-route" />
}
return (
<>
<render component />
</>
)
答案 10 :(得分:1)
编辑:我刚刚意识到警告正在引用名为TextLayerInternal
的组件。这可能是您的错误所在。其余的这些仍然有用,但是可能无法解决您的问题。
1)为此警告获取组件实例非常困难。在React中似乎有一些讨论可以改善此问题,但目前尚无简便方法。我怀疑它尚未构建的原因可能是因为期望组件以这样一种方式编写:无论组件的状态如何,都无法在卸载后使用setState。就React团队而言,问题始终在组件代码中,而不是在组件实例中,这就是为什么要获得组件类型名称的原因。
该答案可能无法令人满意,但是我认为我可以解决您的问题。
2)Lodashes的节流函数具有cancel
方法。在cancel
中呼叫componentWillUnmount
,并抛弃isComponentMounted
。与引入新属性相比,取消更像是“习惯上”的React。
答案 11 :(得分:0)
基于@ 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> ... ;
} ;
答案 12 :(得分:0)
根据您打开网页的方式,可能不会导致安装。例如使用<Link/>
返回虚拟DOM中已挂载的页面,因此会捕获来自componentDidMount生命周期的数据。
答案 13 :(得分:0)
函数式组件方法(Minimal Demo、Full Demo):
import React, { useState } from "react";
import { useAsyncEffect } from "use-async-effect2";
import cpFetch from "cp-fetch"; //cancellable c-promise fetch wrapper
export default function TestComponent(props) {
const [text, setText] = useState("");
useAsyncEffect(
function* () {
setText("fetching...");
const response = yield cpFetch(props.url);
const json = yield response.json();
setText(`Success: ${JSON.stringify(json)}`);
},
[props.url]
);
return <div>{text}</div>;
}
类组件(Live demo)
import React from "react";
import { ReactComponent, timeout } from "c-promise2";
import cpFetch from "cp-fetch";
@ReactComponent
class TestComponent extends React.Component {
state = {
text: "fetching..."
};
@timeout(5000)
*componentDidMount() {
const response = yield cpFetch(this.props.url);
this.setState({ text: `json: ${yield response.text()}` });
}
render() {
return <div>{this.state.text}</div>;
}
}
答案 14 :(得分:0)
我遇到了类似的问题并解决了这个问题:
我通过在redux上调度操作来自动使用户登录 (将身份验证令牌置于redux状态)
然后我试图用this.setState({succ_message:“ ...”) 在我的组件中。
组件看上去空着,并在控制台上出现了相同的错误:“未安装的组件” ..“内存泄漏”等。
在此线程上读完Walter的答案后
我注意到在我的应用程序的“路由”表中, 如果用户已登录,则我组件的路由无效:
{!this.props.user.token &&
<div>
<Route path="/register/:type" exact component={MyComp} />
</div>
}
无论令牌是否存在,我都使该路由可见。
答案 15 :(得分:0)
这是一个简单的解决方案。这个警告是由于当我们在后台执行一些获取请求时(因为一些请求需要一些时间。)并且我们从那个屏幕导航回来然后他们反应无法更新状态。这是用于此的示例代码。在每次状态更新之前写这一行。
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.log('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"
}
})
答案 16 :(得分:0)
我通过提供 useEffect 钩子中使用的所有参数解决了这个问题
代码报告了错误:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId]);
修复后的代码:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId,level]);
可以看到,我提供了所有应该通过的参数(包括级别参数)后问题解决了。
答案 17 :(得分:-1)
受到@ford04 接受的答案的启发,我有更好的方法来处理它,而不是在 useEffect
中使用 useAsync
创建一个返回 componentWillUnmount
回调的新函数:< /p>
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)
},[])
答案 18 :(得分:-1)
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]);
}
};
答案 19 :(得分:-1)
React.useEffect(() => {
let unsubscribe;
const getUser = async () => {
unsubscribe = await auth.onAuthStateChanged((user) => {
if (user) {
const userRef = createUserProfileDocument(user);
userRef
.then((ref) => ref.get())
.then((getData) => {
setCurrentUser({ id: getData.id, ...getData.data() });
});
}
});
};
getUser();
return function cleanup() {
unsubscribe();
};
}, [setCurrentUser]);
答案 20 :(得分:-1)
最简单、最紧凑的解决方案(附有说明)如下所示为单行解决方案。
useEffect(() => { return () => {}; }, []);
上面的 useEffect()
示例返回一个回调函数,触发 React 在更新状态之前完成其生命周期的卸载部分。
这个非常简单的解决方案就是所需要的。此外,它还与 @ford04 和 @sfletche 提供的虚构语法不同。顺便说一句,下面来自 @ford04 的代码片段纯粹是虚构的语法(@sfletche 、 @vinod 、 @guneetgstar 和 @Drew Cordano 使用了完全相同的虚构语法) .
data => { <--- 虚构/虚构的语法
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
我的所有 linter 和我整个团队的所有 linter 都不会接受它,他们会报告 Uncaught SyntaxError: unexpected token: '=>'
。我很惊讶没有人发现虚构的语法。任何参与过此问题线程的人,尤其是支持投票的人,能否向我解释他们是如何找到适合他们的解决方案的?
答案 21 :(得分:-2)
受@ford04 回答的启发,我使用了这个钩子,它也接受成功、错误、finally 和 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 正在从后端执行某种获取操作,则在卸载组件时中止它通常是有意义的(但并非总是如此,有时如果例如您正在将一些数据加载到存储中,您也可以即使组件被卸载也只想完成它)