Javascript 无法对卸载的组件执行 React 状态更新
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/53949393/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Can't perform a React state update on an unmounted component
提问by Igor Soloydenko
Problem
问题
I am writing an application in React and was unable to avoid a super common pitfall, which is calling setState(...)after componentWillUnmount(...).
我正在用 React 编写一个应用程序,但无法避免一个超级常见的陷阱,它setState(...)在componentWillUnmount(...).
I looked very carefully at my code and tried to put some guarding clauses in place, but the problem persisted and I am still observing the warning.
我非常仔细地查看了我的代码并尝试添加一些保护条款,但问题仍然存在,我仍在观察警告。
Therefore, I've got two questions:
因此,我有两个问题:
- How do I figure out from the stack trace, which particular component and event handler or lifecycle hook is responsible for the rule violation?
- Well, how to fix the problem itself, because my code was written with this pitfall in mind and is already trying to prevent it, but some underlying component's still generating the warning.
- 我如何从堆栈跟踪中找出哪个特定的组件和事件处理程序或生命周期钩子负责违反规则?
- 好吧,如何解决问题本身,因为我的代码在编写时就考虑到了这个陷阱,并且已经在尝试防止它发生,但是某些底层组件仍在生成警告。
Browser console
浏览器控制台
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
Code
代码
Book.tsx
书.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
自动宽度PDF.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>
);
}
Update 1: Cancel throttleable function (still no luck)
更新 1:取消节流功能(仍然没有运气)
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;
回答by vinod
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 : -
要删除 - 无法对未安装的组件警告执行 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() {
...
}
}
回答by Peter Lamberg
回答by ic3b3rg
try changing setDivSizeThrottleableto
尝试更改setDivSizeThrottleable为
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
{ leading: false, trailing: true }
);
回答by ford04
Here is a React Hooksspecific solution for
这是一个React Hooks特定的解决方案
Error
错误
Warning: Can't perform a React state update on an unmounted component.
警告:无法对卸载的组件执行 React 状态更新。
Solution
解决方案
You can declare let isMounted = trueinside useEffect, which will be changed in the cleanup callback, as soon as the component is unmounted. Before state updates, you now check this variable conditionally:
您可以声明let isMounted = trueinside 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>
Extension: Custom useAsyncHook
扩展:自定义useAsync挂钩
We can encapsulate all the boilerplate into a custom Hook, that just knows, how to deal with and automatically abort async functions in case the component unmounts before:
我们可以将所有样板封装到一个自定义 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>
回答by Walter
I know that you're not using history, but in my case I was using the useHistoryhook from React Router DOM, which unmounts the component before the state is persisted in my React Context Provider.
我知道您没有使用历史记录,但在我的情况下,我使用了useHistoryReact Router DOM 中的钩子,它在状态保留在我的 React Context Provider 中之前卸载了组件。
To fix this problem I have used the hook withRouternesting the component, in my case export default withRouter(Login), and inside the component const Login = props => { ...; props.history.push("/dashboard"); .... I have also removed the other props.history.pushfrom the component, e.g, if(authorization.token) return props.history.push('/dashboard')because this causes a loop, because the authorizationstate.
为了解决这个问题,我使用了withRouter嵌套组件的钩子,在我的例子中export default withRouter(Login),在组件内部const Login = props => { ...; props.history.push("/dashboard"); ...。我还props.history.push从组件中删除了另一个,例如,if(authorization.token) return props.history.push('/dashboard')因为这会导致循环,因为authorization状态。
An alternative to push a new item to history.
将新项目推送到history的替代方法。
回答by R Esmond
Edit: I just realized the warning is referencing a component called TextLayerInternal. That's likely where your bug is. The rest of this is still relevant, but it might not fix your problem.
编辑:我刚刚意识到警告引用了一个名为TextLayerInternal. 这很可能是您的错误所在。其余部分仍然相关,但它可能无法解决您的问题。
1) Getting the instance of a component for this warning is tough. It looks like there is some discussion to improve this in React but there currently is no easy way to do it. The reason it hasn't been built yet, I suspect, is likely because components are expected to be written in such a way that setState after unmount isn't possible no matter what the state of the component is. The problem, as far as the React team is concerned, is always in the Component code and not the Component instance, which is why you get the Component Type name.
1) 获取此警告的组件实例很困难。看起来有一些讨论可以在 React 中改进这一点,但目前没有简单的方法来做到这一点。我怀疑它尚未构建的原因可能是因为组件应该以这样的方式编写,即无论组件的状态如何,都无法在 unmount 后设置 setState。就 React 团队而言,问题始终出在 Component 代码中,而不是 Component 实例中,这就是您获得 Component Type 名称的原因。
That answer might be unsatisfactory, but I think I can fix your problem.
那个答案可能不令人满意,但我想我可以解决你的问题。
2) Lodashes throttled function has a cancelmethod. Call cancelin componentWillUnmountand ditch the isComponentMounted. Canceling is more "idiomatically" React than introducing a new property.
2)Lodashes 节流函数有一个cancel方法。通话cancel中componentWillUnmount和沟isComponentMounted。取消比引入新属性更“惯用”React。
回答by May Weather VN
If above solutions dont work, try this and it works for me:
如果上述解决方案不起作用,试试这个,它对我有用:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
回答by beemaster
I had a similar problem and solved it :
我有一个类似的问题并解决了它:
I was automatically making the user logged-in by dispatching an action on redux ( placing authentication token on redux state )
我通过在 redux 上分派一个动作来自动让用户登录(将身份验证令牌放在 redux 状态)
and then I was trying to show a message with this.setState({succ_message: "...") in my component.
然后我试图在我的组件中显示带有 this.setState({succ_message: "...") 的消息。
Component was looking empty with the same error on console : "unmounted component".."memory leak" etc.
组件在控制台上看起来是空的,并出现相同的错误:“未安装的组件”..“内存泄漏”等。
After I read Walter's answer up in this thread
在我阅读了 Walter 在此线程中的回答之后
I've noticed that in the Routing table of my application , my component's route wasn't valid if user is logged-in :
我注意到在我的应用程序的路由表中,如果用户登录,我的组件的路由无效:
{!this.props.user.token &&
<div>
<Route path="/register/:type" exact component={MyComp} />
</div>
}
I made the Route visible whether the token exists or not.
无论令牌是否存在,我都使 Route 可见。


