如何取消对组件 WillUnmount 的读取

我觉得标题说明了一切。每次卸载仍在获取的组件时,都会显示黄色警告。

Console

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but ... To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

  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);
});
}
101567 次浏览

当你激活一个承诺,它可能需要几秒钟才能解决,到那个时候用户可能已经导航到另一个地方在您的应用程序。因此,当您在未装载的组件上执行了解析 setState时,您会得到一个错误——就像您的情况一样。这也可能导致内存泄漏。

这就是为什么最好将一些异步逻辑从组件中移除的原因

否则,您将需要以某种方式 取消你的承诺。或者,作为最后的手段(它是一个反模式) ,您可以保留一个变量来检查组件是否仍然挂载:

componentDidMount(){
this.mounted = true;


this.props.fetchData().then((response) => {
if(this.mounted) {
this.setState({ data: response })
}
})
}


componentWillUnmount(){
this.mounted = false;
}

I will stress that again - this 是一个反模式 but may be sufficient in your case (just like they did with Formik implementation).

关于 GitHub的类似讨论

编辑:

这可能是我如何用 钩子解决同样的问题(除了反应什么都没有) :

OPTION 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

当我需要“取消所有订阅和异步”时,我通常会分派一些东西来减少组件 WillUnmount,以通知所有其他订阅者,并在必要时向服务器发送一个关于取消的请求

我想我找到办法了。问题不在于抓取本身,而是在组件被释放之后的 setState。因此,解决方案是将 this.state.isMounted设置为 false,然后在 componentWillMount上将其改为 true,在 componentWillUnmount上再次设置为 false。然后只需 if(this.state.isMounted)取内部的 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,
})
}

此警告的关键在于您的组件有一个对它的引用,该引用由一些未完成的回调/承诺保存。

为了避免像在第二种模式中那样保持 isMounted 状态(保持组件活动)的反模式,response 网站建议使用 用一个可选的承诺; 然而,该代码似乎也保持对象活动。

相反,我通过使用带有嵌套绑定函数的闭包来实现 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.
}
}

React recommend的友好人士用一个可取消的承诺来包装你的取款电话/承诺。虽然文档中没有建议将代码与类或函数分离,但这似乎是明智的,因为其他类和函数可能需要这种功能,代码重复是一种反模式,而且不管延迟的代码应该在 componentWillUnmount()中处理或取消。根据 React,您可以在 componentWillUnmount中的包装承诺上调用 cancel(),以避免在未安装的组件上设置状态。

如果我们使用 React 作为指南,所提供的代码看起来就像下面这些代码片段:

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 来帮助垃圾收集器释放内存。

可以使用 终止控制器取消获取请求。

参见: 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>

Since the post had been opened, an "abortable-fetch" has been added. Https://developers.google.com/web/updates/2017/09/abortable-fetch

(来自文件:)

控制器 + 信号机动 会见终止控制器和终止信号:

const controller = new AbortController();
const signal = controller.signal;

The controller only has one method:

Abort () ; When you do this, it notifies the signal:

signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});

这个 API 是由 DOM 标准提供的,这就是整个 API。它是有意为之的通用性,以便其他 Web 标准和 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);
});

我认为,如果没有必要通知服务器有关取消的信息,最好的方法就是使用异步/等待语法(如果可用的话)。

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);
}
}

除了已接受解决方案中的可取消承诺钩子示例之外,使用 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
}

使用 答应我包,您可以取消承诺链,包括嵌套的承诺链。它支持用于替代 ECMA 异步函数的 AbortController 和生成器。使用 CGoogle 装饰器,您可以轻松地管理异步任务,使它们可以取消。

装修工人的使用方法 现场演示:

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");
}
}

所有的阶段都是完全可以取消/放弃的。 Here is an example of using it with 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>
);
}
}

使用 Hooks 和 cancel方法

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>
);
}

另一种方法是将异步函数封装在一个包装器中,该包装器将在组件卸载时处理用例

我们知道函数在 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
}
}
}

然后在 Component entWillUnmount ()内部简单地调用 promesifiedFunction.abort () this will update the cancel flag and run the reject function

只有四个步骤:

1. create instance of AbortController: : const controller = new AbortController ()

2. 获取信号: : const 信号 = 控制器信号

3. 通过信号提取参数

控制器随时中止:

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()
}

如果有超时,则在卸载组件时清除它们。

useEffect(() => {
getReusableFlows(dispatch, selectedProject);
dispatch(fetchActionEvents());


const timer = setInterval(() => {
setRemaining(getRemainingTime());
}, 1000);


return () => {
clearInterval(timer);
};
}, []);

这里有很多很棒的答案,我也决定加入一些。创建你自己版本的 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);
}

Use it as a normal useEffect with state.safe being available for you in the callback that you pass:

useSafeEffect(({ safe }) => {
// some code
apiCall(args).then(result => {
if (!safe) return;
// updating the state
})
}, [dep1, dep2]);

这是一个更通用的异步/等待和承诺解决方案。 我这样做是因为我的 React 回调处于重要的异步调用之间,所以我不能取消所有的承诺。

// TemporalFns.js
let storedFns = {};
const nothing = () => {};
export const temporalThen = (id, fn) => {
if(!storedFns[id])
storedFns[id] = {total:0}
let pos = storedFns[id].total++;
storedFns[id][pos] = fn;
    

return data => { const res = storedFns[id][pos](data); delete storedFns[id][pos]; return res; }
}
export const cleanTemporals = (id) => {
for(let i = 0; i<storedFns[id].total; i++) storedFns[id][i] = nothing;
}

用法: (显然每个实例应该有不同的 id)

const Test = ({id}) => {
const [data,setData] = useState('');
useEffect(() => {
someAsyncFunction().then(temporalThen(id, data => setData(data))
.then(otherImportantAsyncFunction).catch(...);
return () => { cleanTemporals(id); }
}, [])
return (<p id={id}>{data}</p>);
}

我们可以创建一个自定义钩子来包装提取函数,如下所示:

//my-custom-fetch-hook.js
import {useEffect, useRef} from 'react'
function useFetch(){
const isMounted = useRef(true)


useEffect(() => {
isMounted.current = true   //must set this in useEffect or your will get a error when the debugger refresh the page
return () => {isMounted.current = false}
}, [])




return (url, config) => {
return fetch(url, config).then((res) => {
if(!isMounted.current)
throw('component unmounted')
return res
})
}
}


export default useFetch

然后在我们的功能组件中:

import useFetch from './my-custom-fetch-hook.js'


function MyComponent(){
const fetch = useFetch()
...


fetch(<url>, <config>)
.then(res => res.json())
.then(json => { ...set your local state here})
.catch(err => {...do something})
}