问题
我正在用 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
https://i.stack.imgur.com/ATfMj.png
代码
书.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>
);
}
更新 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;
this.setDivSizeThrottleable.cancel()
而不是 this.isComponentMounted
守卫的建议?
这是一个 React Hooks 特定的解决方案
错误
警告:无法对未安装的组件执行 React 状态更新。
解决方案
您可以在 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 (
卸载Child,在它还在加载的时候,它不会在后面设置状态,所以不会触发错误。
扩展:自定义 useAsync Hook
我们可以将所有样板封装到一个自定义 Hook 中,如果组件卸载或依赖值之前发生更改,它会自动中止异步函数:
function useAsync(asyncFn, onSuccess) {
useEffect(() => {
let isActive = true;
asyncFn().then(data => {
if (isActive) onSuccess(data);
});
return () => { isActive = false };
}, [asyncFn, onSuccess]);
}
// 用于在卸载或依赖项更改时自动中止的自定义 Hook // 您也可以为 Promise 错误添加 onFailure。 function useAsync(asyncFn, onSuccess) { useEffect(() => { let isActive = true; asyncFn().then(data => { if (isActive) onSuccess(data) else console.log("未安装组件上的中止 setState" ) }); return () => { isActive = false; }; }, [asyncFn, onSuccess]); } const Child = () => { const [state, setState] = useState("loading (4 sec)..."); useAsync(simulateFetchData, setState);返回
卸载Child,在它还在加载的时候,它不会在后面设置状态,所以不会触发错误。
有关效果清理的更多信息:Overreacted: A Complete Guide to useEffect
要删除 - 无法对未安装的组件警告执行 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() {
...
}
}
const isMountedComponent = useRef(true); useEffect(() => { if (isMountedComponent.current) { ... } return () => { isMountedComponent.current = false; }; });
_isMounted
不是由 React 管理的(与 state
不同),因此不受 React 的 rendering pipeline 的约束。问题是,当一个组件被设置为卸载时,React 会取消对 setState()
的任何调用(这将触发“重新渲染”);因此,状态永远不会更新
如果上述解决方案不起作用,试试这个,它对我有用:
componentWillUnmount() {
// fix Warning: Can't perform a React state update on an unmounted component
this.setState = (state,callback)=>{
return;
};
}
有一个相当常见的钩子 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); }
})
});
...
}
isMounted
状态将特定于调用 useIsMounted
的每个组件!
useIsMounted
的这种解决方式应该包含在核心包中。
return () =>
会导致任何泄漏吗?
检查组件是否已安装实际上是反模式 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()
}
}, [])
@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;
};
}
}
});
尝试将 setDivSizeThrottleable
更改为
this.setDivSizeThrottleable = throttle(
() => {
if (this.isComponentMounted) {
this.setState({
pdfWidth: this.pdfWrapper!.getBoundingClientRect().width - 5,
});
}
},
500,
{ leading: false, trailing: true }
);
我知道您没有使用历史记录,但在我的情况下,我使用了来自 React Router DOM 的 useHistory
钩子,它会在状态持久保存在我的 React Context Provider 之前卸载组件。
为了解决这个问题,我使用了嵌套组件的钩子 withRouter
,在我的例子中是 export default withRouter(Login)
,并在组件 const Login = props => { ...; props.history.push("/dashboard"); ...
内。我还从组件中删除了另一个 props.history.push
,例如 if(authorization.token) return props.history.push('/dashboard')
,因为这会导致循环,因为 authorization
状态。
将新项目推送到 history 的替代方法。
将 ref 添加到 jsx 组件,然后检查它是否存在
function Book() {
const ref = useRef();
useEffect(() => {
asyncOperation().then(data => {
if (ref.current) setState(data);
})
});
return <div ref={ref}>content</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.
是什么导致错误?
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 />
</>
)
如果您从 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;
};
}, []);
在大多数情况下,isMounted
方法是一种反模式,因为它实际上并没有清理/取消任何东西,它只是避免更改未安装组件的状态,但对挂起的异步任务不做任何事情。 React 团队 recently removed 发出泄漏警告,因为用户不断创建大量反模式来隐藏警告而不是修复其原因。
但是用纯 JS 编写可取消的代码确实很棘手。为了解决这个问题,我使用自定义钩子创建了自己的库 useAsyncEffect2,构建在可取消承诺 (c-promise2) 之上,用于执行可取消异步代码以实现其优雅取消。所有异步阶段(承诺),包括深度阶段,都是可以取消的。这意味着如果其父上下文被取消,此处的请求将自动中止。当然,可以使用任何其他异步操作来代替请求。
具有普通 useState 用法的 useAsyncEffect 演示(现场演示):
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 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>
);
}
使用装饰器的类组件(现场演示)
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;
更多其他示例:
带有错误处理的 Axios 请求
通过坐标获取天气
实时搜索
暂停和恢复
进度捕获
对于这个错误,我有 2 个解决方案:
返回:
如果你使用了hook
和useEffect
,那么在useEffect
后面放一个return
。
useEffect(() => {
window.addEventListener('mousemove', logMouseMove)
return () => {
window.removeEventListener('mousemove', logMouseMove)
}
}, [])
组件将卸载:
如果您使用componentDidMount
,那么将componentWillUnmount
放在它旁边。
componentDidMount() {
window.addEventListener('mousemove', this.logMouseMove)
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.logMouseMove)
}
编辑:我刚刚意识到警告引用了一个名为 TextLayerInternal
的组件。这很可能是你的错误所在。其余部分仍然相关,但它可能无法解决您的问题。
1) 获取此警告的组件实例很困难。看起来有一些讨论可以在 React 中改进这一点,但目前还没有简单的方法来做到这一点。我怀疑它尚未构建的原因可能是因为组件的预期编写方式是,无论组件的状态如何,卸载后的 setState 都是不可能的。就 React 团队而言,问题始终出在 Component 代码中,而不是 Component 实例中,这就是您获得 Component Type 名称的原因。
这个答案可能不令人满意,但我想我可以解决你的问题。
2) Lodashes 节流函数有一个 cancel
方法。在 componentWillUnmount
中调用 cancel
并放弃 isComponentMounted
。取消比引入新属性更“惯用” React。
TextLayerInternal
。因此,我不知道“谁的错是 setState()
电话”。我会按照您的建议尝试 cancel
,看看效果如何,
更新不要使用我的原始答案,因为它不起作用
这个答案是基于使用可取消的承诺和 makecancelable 中的一个注释,我迁移到使用钩子。但是,它似乎不会取消 async
/await
甚至 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 中起作用的例子。
accepted answer 是最好的。由于我使用 TypeScript,我对代码进行了一些修改(我明确设置了依赖项,因为接受的答案的隐式依赖项似乎在我的应用程序上提供了重新渲染循环,添加并使用 async/await 而不是 promise 链,传递一个引用挂载的对象,以便在需要时可以提前取消异步/等待链)
/**
* 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);
}
基于@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> ... ;
} ;
根据您打开网页的方式,您可能不会导致安装。例如使用 <Link/>
返回到已经安装在虚拟 DOM 中的页面,因此需要来自 componentDidMount 生命周期的数据被捕获。
componentDidMount()
可以被调用两次而没有中间的 componentWillUnmount()
调用?我不认为这是可能的。
<Link/>
时页面不处理 componentDidMount()
内的代码的原因。我使用 Redux 来解决这些问题,并将网页的数据保存在 Reducer 存储中,这样我就不需要重新加载页面。
这是一个简单的解决方案。这个警告是由于当我们在后台执行一些获取请求时(因为一些请求需要一些时间。)并且我们从那个屏幕导航回来,然后他们反应不能更新状态。这是示例代码。在每个状态更新之前写下这一行。
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]);
可以看到,在我提供了所有应该通过的参数(包括级别参数)后,问题就解决了。
我有一个类似的问题并解决了它:
我通过在 redux 上发送一个操作来自动让用户登录(将身份验证令牌放在 redux 状态)
然后我试图在我的组件中显示一条带有 this.setState({succ_message: "...") 的消息。
组件在控制台上看起来是空的,出现相同的错误:“未安装的组件”..“内存泄漏”等。
在我阅读了沃尔特在这个帖子中的回答之后
我注意到在我的应用程序的路由表中,如果用户登录,我的组件的路由无效:
{!this.props.user.token &&
<div>
<Route path="/register/:type" exact component={MyComp} />
</div>
}
无论令牌是否存在,我都使 Route 可见。
受@ford04 接受的答案的启发,我有更好的方法来处理它,而不是在 useAsync
中使用 useEffect
创建一个返回回调的新函数 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
挂钩)。
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]);
}
};
下面将最简单和最紧凑的解决方案(带有解释)视为单线解决方案。
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: '=>'
。我很惊讶没有人掌握想象的语法。任何参与过这个问题线程的人,尤其是支持投票的人,会向我解释他们是如何找到适合他们的解决方案的吗?
someAsynchronousOperation()
返回值类型是 Promise<void>
,那么肯定 data
将导致 TypeScript 编译错误。但是,如果 X
不是 undefined
/void
/never
是 Promise<X>
,那么您肯定可以使用 .then(data => {...})
!您还没有提供一个完整的最小示例来推理它。如果您希望解决您的特定代码问题,请在 StackOverflow 上打开一个单独的问题。您不想被否决或标记答案。
受@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 正在从后端进行某种获取,则在卸载组件时中止它通常是有意义的(但并非总是如此,有时,如果您正在将一些数据加载到存储中,那么您可能只想即使组件未安装也完成它)
不定期副业成功案例分享
isMounted
标志切换为false
的理想场所,可以从周围的效果回调闭包范围访问。您可以将清理函数视为 belonging to 的相应效果。useEffect
中的fetch
和 b) 不稳定,即可能在异步结果返回之前卸载并准备好设置为状态。useCallback
是 React 中将依赖关系的责任推迟到useAsync
的客户端的常用且推荐的方式。您可以切换到useAsync
内的可变引用来存储最近的回调,因此客户端可以直接传递他们的函数/回调而无需依赖。但我会谨慎使用这种模式,因为它可能更令人困惑和命令式的方法。