如何取消对 componentWillUnmount 的提取
Posted
技术标签:
【中文标题】如何取消对 componentWillUnmount 的提取【英文标题】:How to cancel a fetch on componentWillUnmount 【发布时间】:2018-09-29 02:13:57 【问题描述】:我认为标题说明了一切。每次我卸载仍在获取的组件时都会显示黄色警告。
安慰警告:无法在未安装的组件上调用
setState
(或forceUpdate
)。这是一个无操作,但是...要修复,请取消componentWillUnmount
方法中的所有订阅和异步任务。
constructor(props)
super(props);
this.state =
isLoading: true,
dataSource: [
name: 'loading...',
id: 'loading',
]
componentDidMount()
return fetch('LINK HERE')
.then((response) => response.json())
.then((responseJson) =>
this.setState(
isLoading: false,
dataSource: responseJson,
, function()
);
)
.catch((error) =>
console.error(error);
);
【问题讨论】:
警告我没有这个问题 问题已更新 你是否承诺或异步获取代码 加你获取代码到qustion 见isMounted is an Antipattern和aborting a fetch。 【参考方案1】:当您触发 Promise 时,它可能需要几秒钟才能解决,到那时用户可能已经导航到您应用中的另一个位置。因此,当 Promise 解决 setState
在未安装的组件上执行时,您会收到错误 - 就像您的情况一样。这也可能导致内存泄漏。
这就是为什么最好将一些异步逻辑移出组件的原因。
否则,您将需要以某种方式cancel your Promise。或者——作为最后的手段(它是一种反模式)——你可以保留一个变量来检查组件是否仍然挂载:
componentDidMount()
this.mounted = true;
this.props.fetchData().then((response) =>
if(this.mounted)
this.setState( data: response )
)
componentWillUnmount()
this.mounted = false;
我会再次强调这一点 - 这个 is an antipattern 但在你的情况下可能就足够了(就像他们对 Formik
实现所做的那样)。
GitHub上的类似讨论
编辑:
这可能是我如何用Hooks 解决同样的问题(除了 React 什么都没有):
选项 A:
import React, useState, useEffect from "react";
export default function Page()
const value = usePromise("https://something.com/api/");
return (
<p>value ? value : "fetching data..."</p>
);
function usePromise(url)
const [value, setState] = useState(null);
useEffect(() =>
let isMounted = true; // track whether component is mounted
request.get(url)
.then(result =>
if (isMounted)
setState(result);
);
return () =>
// clean up
isMounted = false;
;
, []); // only on "didMount"
return value;
选项 B: 或者使用 useRef
,它的行为类似于类的静态属性,这意味着它不会在值更改时重新渲染组件:
function usePromise2(url)
const isMounted = React.useRef(true)
const [value, setState] = useState(null);
useEffect(() =>
return () =>
isMounted.current = false;
;
, []);
useEffect(() =>
request.get(url)
.then(result =>
if (isMounted.current)
setState(result);
);
, []);
return value;
// or extract it to custom hook:
function useIsMounted()
const isMounted = React.useRef(true)
useEffect(() =>
return () =>
isMounted.current = false;
;
, []);
return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
示例:https://codesandbox.io/s/86n1wq2z8
【讨论】:
所以没有真正的方法可以取消对 componentWillUnmount 的提取? 哦,我之前没有注意到你的答案的代码,它确实有效。谢谢 见isMounted is an Antipattern和aborting a fetch。 “这就是为什么最好将异步逻辑移出组件。”是什么意思?反应中的一切不都是一个组件吗? @Karpik 我的意思是使用 redux 或 mobx 或其他状态管理库。然而,像 react-suspense 这样的新功能可能会解决这个问题。【参考方案2】:React recommend 的友好人员将您的 fetch 调用/承诺包装在可取消的承诺中。虽然该文档中没有建议使用 fetch 将代码与类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,代码重复是一种反模式,并且不管挥之不去的代码应在componentWillUnmount()
中处理或取消。根据 React,您可以在 componentWillUnmount
中的包装承诺上调用 cancel()
以避免在未安装的组件上设置状态。
如果我们使用 React 作为指导,所提供的代码将类似于以下代码 sn-ps:
const makeCancelable = (promise) =>
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) =>
promise.then(
val => hasCanceled_ ? reject(isCanceled: true) : resolve(val),
error => hasCanceled_ ? reject(isCanceled: true) : reject(error)
);
);
return
promise: wrappedPromise,
cancel()
hasCanceled_ = true;
,
;
;
const cancelablePromise = makeCancelable(fetch('LINK HERE'));
constructor(props)
super(props);
this.state =
isLoading: true,
dataSource: [
name: 'loading...',
id: 'loading',
]
componentDidMount()
cancelablePromise.
.then((response) => response.json())
.then((responseJson) =>
this.setState(
isLoading: false,
dataSource: responseJson,
, () =>
);
)
.catch((error) =>
console.error(error);
);
componentWillUnmount()
cancelablePromise.cancel();
---- 编辑----
通过关注 GitHub 上的问题,我发现给定的答案可能并不完全正确。这是我使用的一个适用于我的目的的版本:
export const makeCancelableFunction = (fn) =>
let hasCanceled = false;
return
promise: (val) => new Promise((resolve, reject) =>
if (hasCanceled)
fn = null;
else
fn(val);
resolve(val);
),
cancel()
hasCanceled = true;
;
;
这个想法是通过使函数或您使用的任何内容为 null 来帮助垃圾收集器释放内存。
【讨论】:
你有github上问题的链接吗 @Ren,有一个 GitHub site 用于编辑页面和讨论问题。 我不再确定 GitHub 项目的确切问题出在哪里。 GitHub 问题链接:github.com/facebook/react/issues/5465【参考方案3】:您可以使用AbortController 取消获取请求。
另见:https://www.npmjs.com/package/abortcontroller-polyfill
class FetchComponent extends React.Component
state = todos: [] ;
controller = new AbortController();
componentDidMount()
fetch('https://jsonplaceholder.typicode.com/todos',
signal: this.controller.signal
)
.then(res => res.json())
.then(todos => this.setState( todos ))
.catch(e => alert(e.message));
componentWillUnmount()
this.controller.abort();
render()
return null;
class App extends React.Component
state = fetch: true ;
componentDidMount()
this.setState( fetch: false );
render()
return this.state.fetch && <FetchComponent/>
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
【讨论】:
我希望我知道有一个用于取消请求的 Web API,例如 AbortController。不过好吧,现在知道还为时不晚。谢谢。 那么如果你有多个fetch
es,你能把那个AbortController
传给他们所有人吗?【参考方案4】:
自从帖子被打开后,添加了一个“abortable-fetch”。 https://developers.google.com/web/updates/2017/09/abortable-fetch
(来自文档:)
控制器+信号机动 认识 AbortController 和 AbortSignal:
const controller = new AbortController();
const signal = controller.signal;
控制器只有一种方法:
controller.abort(); 当你这样做时,它会通知信号:
signal.addEventListener('abort', () =>
// Logs true:
console.log(signal.aborted);
);
此 API 由 DOM 标准提供,这就是整个 API。它是特意通用的,因此可以被其他网络标准和 javascript 库使用。
例如,以下是 5 秒后设置获取超时的方法:
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000);
fetch(url, signal ).then(response =>
return response.text();
).then(text =>
console.log(text);
);
【讨论】:
有趣,我会尝试这种方式。但在此之前,我会先阅读 AbortController API。 我们是否可以只使用一个 AbortController 实例进行多次提取,这样当我们在 componentWillUnmount 中调用单个 AbortController 的 abort 方法时,它会取消我们组件中所有现有的提取?如果没有,这意味着我们必须为每个提取提供不同的 AbortController 实例,对吧? @LexSoft 您找到问题的答案了吗? @Superdude 答案是肯定的【参考方案5】:这个警告的关键是你的组件有一个对它的引用,它被一些未完成的回调/承诺所持有。
为了避免像第二种模式那样保持 isMounted 状态的反模式(使组件保持活动状态),react 网站建议using an optional promise;但是,该代码似乎也使您的对象保持活动状态。
相反,我通过使用带有嵌套绑定函数到 setState 的闭包来完成。
这是我的构造函数(打字稿)...
constructor(props: any, context?: any)
super(props, context);
let cancellable =
// it's important that this is one level down, so we can drop the
// reference to the entire object by setting it to undefined.
setState: this.setState.bind(this)
;
this.componentDidMount = async () =>
let result = await fetch(…);
// ideally we'd like optional chaining
// cancellable.setState?.( url: result || '' );
cancellable.setState && cancellable.setState( url: result || '' );
this.componentWillUnmount = () =>
cancellable.setState = undefined; // drop all references.
【讨论】:
这在概念上与保留一个 isMounted 标志没有什么不同,只是你将它绑定到闭包而不是挂在this
【参考方案6】:
当我需要“取消所有订阅和异步”时,我通常会在 componentWillUnmount 中向 redux 发送一些内容,以通知所有其他订阅者,并在必要时向服务器发送一个关于取消的请求
【讨论】:
【参考方案7】:我认为如果不需要通知服务器取消 - 最好的方法就是使用 async/await 语法(如果可用)。
constructor(props)
super(props);
this.state =
isLoading: true,
dataSource: [
name: 'loading...',
id: 'loading',
]
async componentDidMount()
try
const responseJson = await fetch('LINK HERE')
.then((response) => response.json());
this.setState(
isLoading: false,
dataSource: responseJson,
catch
console.error(error);
【讨论】:
【参考方案8】:除了可接受的解决方案中的可取消承诺挂钩示例之外,使用 useAsyncCallback
挂钩包装请求回调并返回可取消承诺会很方便。想法是一样的,但钩子的工作方式与普通的useCallback
一样。下面是一个实现示例:
function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[])
const isMounted = useRef(true)
useEffect(() =>
return () =>
isMounted.current = false
, [])
const cb = useCallback(callback, dependencies)
const cancellableCallback = useCallback(
(...args: any[]) =>
new Promise<T>((resolve, reject) =>
cb(...args).then(
value => (isMounted.current ? resolve(value) : reject( isCanceled: true )),
error => (isMounted.current ? reject(error) : reject( isCanceled: true ))
)
),
[cb]
)
return cancellableCallback
【讨论】:
【参考方案9】:另一种替代方法是将异步函数包装在一个包装器中,该包装器将在组件卸载时处理用例
我们知道函数也是 js 中的对象,所以我们可以使用它们来更新闭包值
const promesifiedFunction1 = (func) =>
return function promesify(...agrs)
let cancel = false;
promesify.abort = ()=>
cancel = true;
return new Promise((resolve, reject)=>
function callback(error, value)
if(cancel)
reject(cancel:true)
error ? reject(error) : resolve(value);
agrs.push(callback);
func.apply(this,agrs)
)
//here param func pass as callback should return a promise object
//example fetch browser API
//const fetchWithAbort = promesifiedFunction2(fetch)
//use it as fetchWithAbort('http://example.com/movies.json',...options)
//later in componentWillUnmount fetchWithAbort.abort()
const promesifiedFunction2 = (func)=>
return async function promesify(...agrs)
let cancel = false;
promesify.abort = ()=>
cancel = true;
try
const fulfilledValue = await func.apply(this,agrs);
if(cancel)
throw 'component un mounted'
else
return fulfilledValue;
catch (rejectedValue)
return rejectedValue
然后在 componentWillUnmount() 内部简单地调用 promesifiedFunction.abort() 这将更新取消标志并运行拒绝功能
【讨论】:
【参考方案10】:使用CPromise 包,您可以取消您的承诺链,包括嵌套的。它支持 AbortController 和生成器作为 ECMA 异步函数的替代品。使用 CPromise 装饰器,您可以轻松管理异步任务,使其可取消。
装饰器使用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()
console.log("mounted");
const response = yield cpFetch(this.props.url);
this.setState( text: `json: $yield response.text()` );
render()
return <div>this.state.text</div>;
componentWillUnmount()
console.log("unmounted");
那里的所有阶段都是完全可取消/可中止的。 这是一个使用 React 的例子Live Demo
import React, Component from "react";
import
CPromise,
CanceledError,
ReactComponent,
E_REASON_UNMOUNTED,
listen,
cancel
from "c-promise2";
import cpAxios from "cp-axios";
@ReactComponent
class TestComponent extends Component
state =
text: ""
;
*componentDidMount(scope)
console.log("mount");
scope.onCancel((err) => console.log(`Cancel: $err`));
yield CPromise.delay(3000);
@listen
*fetch()
this.setState( text: "fetching..." );
try
const response = yield cpAxios(this.props.url).timeout(
this.props.timeout
);
this.setState( text: JSON.stringify(response.data, null, 2) );
catch (err)
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
this.setState( text: err.toString() );
*componentWillUnmount()
console.log("unmount");
render()
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>this.state.text</div>
<button
className="btn btn-success"
type="submit"
onClick=() => this.fetch(Math.round(Math.random() * 200))
>
Fetch random character info
</button>
<button
className="btn btn-warning"
onClick=() => cancel.call(this, "oops!")
>
Cancel request
</button>
</div>
);
Using Hooks and cancel
method
import React, useState from "react";
import
useAsyncEffect,
E_REASON_UNMOUNTED,
CanceledError
from "use-async-effect2";
import cpAxios from "cp-axios";
export default function TestComponent(props)
const [text, setText] = useState("");
const [id, setId] = useState(1);
const cancel = useAsyncEffect(
function* ()
setText("fetching...");
try
const response = yield cpAxios(
`https://rickandmortyapi.com/api/character/$id`
).timeout(props.timeout);
setText(JSON.stringify(response.data, null, 2));
catch (err)
CanceledError.rethrow(err, E_REASON_UNMOUNTED);
setText(err.toString());
,
[id]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>text</div>
<button
className="btn btn-success"
type="submit"
onClick=() => setId(Math.round(Math.random() * 200))
>
Fetch random character info
</button>
<button className="btn btn-warning" onClick=cancel>
Cancel request
</button>
</div>
);
【讨论】:
【参考方案11】:只需四个步骤:
1.create instance of AbortController::const controller = new AbortController()
2.get signal:: const signal = controller.signal
3.通过信号获取参数
4.controller随时中止::controller.abort();
const controller = new AbortController()
const signal = controller.signal
function beginFetching()
var urlToFetch = "https://xyxabc.com/api/tt";
fetch(urlToFetch,
method: 'get',
signal: signal,
)
.then(function(response)
console.log('Fetch complete');
).catch(function(err)
console.error(` Err: $err`);
);
function abortFetching()
controller.abort()
【讨论】:
【参考方案12】:如果组件卸载时超时清除它们。
useEffect(() =>
getReusableFlows(dispatch, selectedProject);
dispatch(fetchActionEvents());
const timer = setInterval(() =>
setRemaining(getRemainingTime());
, 1000);
return () =>
clearInterval(timer);
;
, []);
【讨论】:
【参考方案13】:这里有很多很棒的答案,我决定也加入一些。创建自己的 useEffect 版本来消除重复非常简单:
import useEffect from 'react';
function useSafeEffect(fn, deps = null)
useEffect(() =>
const state = safe: true ;
const cleanup = fn(state);
return () =>
state.safe = false;
cleanup?.();
;
, deps);
将其用作正常的 useEffect 并在您传递的回调中为您提供state.safe
:
useSafeEffect(( safe ) =>
// some code
apiCall(args).then(result =>
if (!safe) return;
// updating the state
)
, [dep1, dep2]);
【讨论】:
【参考方案14】:我想我想出了一个办法。问题不在于获取本身,而在于组件关闭后的 setState 。所以解决方案是将this.state.isMounted
设置为false
,然后在componentWillMount
上将其更改为true,并在componentWillUnmount
中再次设置为false。然后只需if(this.state.isMounted)
fetch 中的 setState。像这样:
constructor(props)
super(props);
this.state =
isMounted: false,
isLoading: true,
dataSource: [
name: 'loading...',
id: 'loading',
]
componentDidMount()
this.setState(
isMounted: true,
)
return fetch('LINK HERE')
.then((response) => response.json())
.then((responseJson) =>
if(this.state.isMounted)
this.setState(
isLoading: false,
dataSource: responseJson,
, function()
);
)
.catch((error) =>
console.error(error);
);
componentWillUnmount()
this.setState(
isMounted: false,
)
【讨论】:
setState 可能并不理想,因为它不会立即更新 state 中的值。以上是关于如何取消对 componentWillUnmount 的提取的主要内容,如果未能解决你的问题,请参考以下文章
我如何对 youtube 嵌入播放器进行编程以在单击时取消静音