无法对未安装的组件执行 React 状态更新
Can't perform a React state update on an unmounted component
问题
我正在用 React 编写应用程序,无法避免一个超级常见的陷阱,即在 componentWillUnmount(...)
之后调用 setState(...)
。
我非常仔细地查看了我的代码并尝试放置一些保护子句,但问题仍然存在,我仍在观察警告。
因此,我有两个问题:
- 我如何从堆栈跟踪中找出,哪个特定组件和事件处理程序或生命周期挂钩对规则违规负责?
- 好吧,如何解决问题本身,因为我的代码是在考虑到这个陷阱的情况下编写的,并且已经在努力防止它,但是一些底层组件仍在生成警告。
浏览器控制台
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;
编辑:我刚刚意识到警告引用了一个名为 TextLayerInternal
的组件。那很可能是你的错误所在。其余部分仍然相关,但可能无法解决您的问题。
1) 获取此警告的组件实例很困难。看起来有一些关于在 React 中改进这一点的讨论,但目前还没有简单的方法来做到这一点。它尚未构建的原因,我怀疑,可能是因为组件应该以这样的方式编写,即无论组件的状态如何,卸载后的 setState 都是不可能的。就 React 团队而言,问题始终出在组件代码中,而不是组件实例中,这就是您获得组件类型名称的原因。
这个答案可能不能令人满意,但我想我可以解决你的问题。
2) Lodashes 节流函数有一个 cancel
方法。在 componentWillUnmount
中调用 cancel
并放弃 isComponentMounted
。与引入新的 属性.
相比,取消更 "idiomatically" React
尝试将 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
(这在这 3 issues linked together 中讨论)。
无论如何,升级反应版本消除了警告。
我知道你没有使用历史记录,但在我的例子中,我使用的是来自 React Router DOM 的 useHistory
钩子,它在我的 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
状态。
将新项目推送到 的替代方法。
这里有一个React Hooks具体解决方案
错误
Warning: Can't perform a React state update on an unmounted component.
解决方案
您可以在 useEffect
中声明 let isMounted = true
,一旦卸载组件,它将在 cleanup callback 中更改。在状态更新之前,您现在有条件地检查这个变量:
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.log("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 中,如果组件卸载或依赖值之前已更改,它会自动中止异步函数:
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.log("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>
关于效果清理的更多信息:Overreacted: A Complete Guide to useEffect
如果上述解决方案不起作用,试试这个对我有用:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
我遇到了类似的问题并解决了:
我通过在 redux 上发送一个动作来自动让用户登录
(将身份验证令牌置于 redux 状态)
然后我试图用 this.setState({succ_message: "...") 显示一条消息
在我的组件中。
组件在控制台上看起来是空的,并出现相同的错误:"unmounted component".."memory leak" 等
在我阅读了 Walter 在此主题中的回答后
我注意到在我的应用程序的路由 table 中,
如果用户已登录,我的组件的路由无效:
{!this.props.user.token &&
<div>
<Route path="/register/:type" exact component={MyComp} />
</div>
}
无论令牌是否存在,我都使路由可见。
我遇到了类似的问题,感谢@ford04 帮助我。
但是,又出现了一个错误。
注意。我正在使用 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 />
</>
)
基于@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取数据,还是报错,只要把setter包在condition
里面
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;
};
}, []);
根据您打开网页的方式,您可能不会导致安装。例如使用 <Link/>
返回一个已经挂载在虚拟 DOM 中的页面,因此需要捕获来自 componentDidMount 生命周期的数据。
有一个名为 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); }
})
});
...
}
受@ford04 接受的答案的启发,我有更好的方法来处理它,而不是在 useAsync
中使用 useEffect
创建一个新函数,该函数 returns 为 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)
},[])
受@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
方法在大多数情况下是一种反模式,因为它实际上并没有清理 up/cancel 任何东西,它只是避免更改未安装组件的状态,但对挂起的异步任务没有任何作用. React 团队 recently removed 泄漏警告是因为用户不断创建大量反模式来隐藏警告而不是修复其原因。
但是用纯 JS 编写可取消代码真的很棘手。为了解决这个问题,我制作了自己的库 useAsyncEffect2 with custom hooks, built on top of a cancellable promise (c-promise2) 来执行可取消的异步代码以实现其优雅的取消。所有异步阶段(承诺),包括深层阶段,都是可取消的。这意味着如果其父上下文被取消,此处的请求将自动中止。当然,可以使用任何其他异步操作来代替请求。
useAsyncEffect
具有简单 useState
用法的演示 (Live Demo):
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
使用内部状态的演示 (Live Demo):
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>
);
}
- Class 组件使用装饰器 (Live demo)
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(例如多个 useEffect),建议使用 Ref,如下所示:
- 基本包
"dependencies":
{
"react": "17.0.1",
}
"devDependencies": {
"typescript": "4.1.5",
}
- 我的 Hook 组件
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()
示例 returns 回调函数触发 React 在更新状态之前完成其生命周期的卸载部分。
只需要这个非常简单的解决方案。此外,它的工作方式也不同于 @ford04 和 @sfletche 提供的虚构语法。顺便说一句,下面来自 @ford04 is purely imaginary syntax (@sfletche , @vinod , @guneetgstar , and @Drew Cordano 的代码片段使用了完全相同的假想语法)。
数据 => { <--- Fictional/Imaginary 语法
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
我所有的 linters 和我整个团队的所有 linters 都不接受它,他们报告 Uncaught SyntaxError: unexpected token: '=>'
。我很惊讶没有人抓住虚构的语法。任何参与过这个问题线程的人,尤其是支持投票的人,能否向我解释一下他们是如何找到适合他们的解决方案的?
更新不要使用我的原始答案,因为它不起作用
此答案基于可取消承诺的使用和 makecancelable which I migrated to use hooks. However, it appears it does not cancel a chain of async
/await
and even cancelable-promise
does not support canceling of a chain of awaits
中的注释
对此进行更多研究,似乎 some internal Google reasons prevented cancelable promises from coming into the standard。
此外,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.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"
}
})
我通过提供 useEffect 挂钩中使用的所有参数解决了这个问题
代码报告错误:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId]);
修复后的代码:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId,level]);
可以看到,在我提供了应该通过的所有参数(包括级别参数)后问题解决了。
添加一个 ref 到一个 jsx 组件然后检查它是否存在
function Book() {
const ref = useRef();
useEffect(() => {
asyncOperation().then(data => {
if (ref.current) setState(data);
})
});
return <div ref={ref}>content</div>
}
检查组件是否已安装实际上是一种反模式as per React documentation。 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,你可以在其中实现 AbortController API 来处理它。
对于更多上下文,检查组件是否已安装是一种反模式,因为 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()
}
}, [])
对于这个错误我有 2 个解决方案:
- return:
如果你用的是hook
和useEffect
,那么在useEffect
的最后放一个return
。
useEffect(() => {
window.addEventListener('mousemove', logMouseMove)
return () => {
window.removeEventListener('mousemove', logMouseMove)
}
}, [])
- componentWillUnmount:
如果你用的是componentDidMount
,那就在旁边放componentWillUnmount
。
componentDidMount() {
window.addEventListener('mousemove', this.logMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.logMouseMove)
}
问题
我正在用 React 编写应用程序,无法避免一个超级常见的陷阱,即在 componentWillUnmount(...)
之后调用 setState(...)
。
我非常仔细地查看了我的代码并尝试放置一些保护子句,但问题仍然存在,我仍在观察警告。
因此,我有两个问题:
- 我如何从堆栈跟踪中找出,哪个特定组件和事件处理程序或生命周期挂钩对规则违规负责?
- 好吧,如何解决问题本身,因为我的代码是在考虑到这个陷阱的情况下编写的,并且已经在努力防止它,但是一些底层组件仍在生成警告。
浏览器控制台
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;
编辑:我刚刚意识到警告引用了一个名为 TextLayerInternal
的组件。那很可能是你的错误所在。其余部分仍然相关,但可能无法解决您的问题。
1) 获取此警告的组件实例很困难。看起来有一些关于在 React 中改进这一点的讨论,但目前还没有简单的方法来做到这一点。它尚未构建的原因,我怀疑,可能是因为组件应该以这样的方式编写,即无论组件的状态如何,卸载后的 setState 都是不可能的。就 React 团队而言,问题始终出在组件代码中,而不是组件实例中,这就是您获得组件类型名称的原因。
这个答案可能不能令人满意,但我想我可以解决你的问题。
2) Lodashes 节流函数有一个 cancel
方法。在 componentWillUnmount
中调用 cancel
并放弃 isComponentMounted
。与引入新的 属性.
尝试将 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
(这在这 3 issues linked together 中讨论)。
无论如何,升级反应版本消除了警告。
我知道你没有使用历史记录,但在我的例子中,我使用的是来自 React Router DOM 的 useHistory
钩子,它在我的 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
状态。
将新项目推送到
这里有一个React Hooks具体解决方案
错误
Warning: Can't perform a React state update on an unmounted component.
解决方案
您可以在 useEffect
中声明 let isMounted = true
,一旦卸载组件,它将在 cleanup callback 中更改。在状态更新之前,您现在有条件地检查这个变量:
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.log("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 中,如果组件卸载或依赖值之前已更改,它会自动中止异步函数:
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.log("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>
关于效果清理的更多信息:Overreacted: A Complete Guide to useEffect
如果上述解决方案不起作用,试试这个对我有用:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
我遇到了类似的问题并解决了:
我通过在 redux 上发送一个动作来自动让用户登录 (将身份验证令牌置于 redux 状态)
然后我试图用 this.setState({succ_message: "...") 显示一条消息 在我的组件中。
组件在控制台上看起来是空的,并出现相同的错误:"unmounted component".."memory leak" 等
在我阅读了 Walter 在此主题中的回答后
我注意到在我的应用程序的路由 table 中, 如果用户已登录,我的组件的路由无效:
{!this.props.user.token &&
<div>
<Route path="/register/:type" exact component={MyComp} />
</div>
}
无论令牌是否存在,我都使路由可见。
我遇到了类似的问题,感谢@ford04 帮助我。
但是,又出现了一个错误。
注意。我正在使用 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 />
</>
)
基于@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取数据,还是报错,只要把setter包在condition
里面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;
};
}, []);
根据您打开网页的方式,您可能不会导致安装。例如使用 <Link/>
返回一个已经挂载在虚拟 DOM 中的页面,因此需要捕获来自 componentDidMount 生命周期的数据。
有一个名为 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); }
})
});
...
}
受@ford04 接受的答案的启发,我有更好的方法来处理它,而不是在 useAsync
中使用 useEffect
创建一个新函数,该函数 returns 为 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)
},[])
受@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
方法在大多数情况下是一种反模式,因为它实际上并没有清理 up/cancel 任何东西,它只是避免更改未安装组件的状态,但对挂起的异步任务没有任何作用. React 团队 recently removed 泄漏警告是因为用户不断创建大量反模式来隐藏警告而不是修复其原因。
但是用纯 JS 编写可取消代码真的很棘手。为了解决这个问题,我制作了自己的库 useAsyncEffect2 with custom hooks, built on top of a cancellable promise (c-promise2) 来执行可取消的异步代码以实现其优雅的取消。所有异步阶段(承诺),包括深层阶段,都是可取消的。这意味着如果其父上下文被取消,此处的请求将自动中止。当然,可以使用任何其他异步操作来代替请求。
useAsyncEffect
具有简单useState
用法的演示 (Live Demo):
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
使用内部状态的演示 (Live Demo):
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>
);
}
- Class 组件使用装饰器 (Live demo)
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(例如多个 useEffect),建议使用 Ref,如下所示:
- 基本包
"dependencies":
{
"react": "17.0.1",
}
"devDependencies": {
"typescript": "4.1.5",
}
- 我的 Hook 组件
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()
示例 returns 回调函数触发 React 在更新状态之前完成其生命周期的卸载部分。
只需要这个非常简单的解决方案。此外,它的工作方式也不同于 @ford04 和 @sfletche 提供的虚构语法。顺便说一句,下面来自 @ford04 is purely imaginary syntax (@sfletche , @vinod , @guneetgstar , and @Drew Cordano 的代码片段使用了完全相同的假想语法)。
数据 => { <--- Fictional/Imaginary 语法
someAsyncOperation().then(data => {
if (isMounted) setState(data); // add conditional check
})
我所有的 linters 和我整个团队的所有 linters 都不接受它,他们报告 Uncaught SyntaxError: unexpected token: '=>'
。我很惊讶没有人抓住虚构的语法。任何参与过这个问题线程的人,尤其是支持投票的人,能否向我解释一下他们是如何找到适合他们的解决方案的?
更新不要使用我的原始答案,因为它不起作用
此答案基于可取消承诺的使用和 makecancelable which I migrated to use hooks. However, it appears it does not cancel a chain of async
/await
and even cancelable-promise
does not support canceling of a chain of awaits
对此进行更多研究,似乎 some internal Google reasons prevented cancelable promises from coming into the standard。
此外,Bluebird 有一些承诺,它引入了可取消的承诺,但它在 Expo 中不起作用,或者至少我还没有看到它在 Expo 中起作用的例子。
/**
* 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.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"
}
})
我通过提供 useEffect 挂钩中使用的所有参数解决了这个问题
代码报告错误:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId]);
修复后的代码:
useEffect(() => {
getDistrict({
geonameid: countryId,
subdistrict: level,
}).then((res) => {
......
});
}, [countryId,level]);
可以看到,在我提供了应该通过的所有参数(包括级别参数)后问题解决了。
添加一个 ref 到一个 jsx 组件然后检查它是否存在
function Book() {
const ref = useRef();
useEffect(() => {
asyncOperation().then(data => {
if (ref.current) setState(data);
})
});
return <div ref={ref}>content</div>
}
检查组件是否已安装实际上是一种反模式as per React documentation。 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,你可以在其中实现 AbortController API 来处理它。
对于更多上下文,检查组件是否已安装是一种反模式,因为 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()
}
}, [])
对于这个错误我有 2 个解决方案:
- return:
如果你用的是hook
和useEffect
,那么在useEffect
的最后放一个return
。
useEffect(() => {
window.addEventListener('mousemove', logMouseMove)
return () => {
window.removeEventListener('mousemove', logMouseMove)
}
}, [])
- componentWillUnmount:
如果你用的是componentDidMount
,那就在旁边放componentWillUnmount
。
componentDidMount() {
window.addEventListener('mousemove', this.logMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.logMouseMove)
}