I think the title says it all. The yellow warning is displayed every time I unmount a component that is still fetching.
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);
});
}
答案 0 :(得分:40)
当您触发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(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
request.get(url)
.then(result => {
if (isMounted.current) {
setState(result);
}
});
}, []);
return value;
}
答案 1 :(得分:14)
React recommend的友好人员将您的获取呼叫/承诺包装在可取消的承诺中。尽管在该文档中没有建议通过获取将代码与类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,但是代码重复是一种反模式,无论代码如何缠结应该在componentWillUnmount()
中处置或取消。按照React的要求,您可以在cancel()
中的包装承诺中调用componentWillUnmount
,以避免在未安装的组件上设置状态。
如果使用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来帮助垃圾收集器释放内存。
答案 2 :(得分:10)
您可以使用AbortController取消获取请求。
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>
答案 3 :(得分:7)
由于该职位已被打开,所以添加了“ 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。它是有意通用的,因此可以被其他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);
});
答案 4 :(得分:3)
当我需要“取消所有订阅和异步”时,我通常会在componentWillUnmount中向redux发送一些内容以通知所有其他订阅者,并在必要时向服务器发送一个有关取消的请求
答案 5 :(得分:3)
这个警告的关键在于你的组件有一个引用它的一些未完成的回调/承诺。
为避免反模式保持你的isMounted状态(使你的组件保持活着),就像在第二种模式中所做的那样,反应网站建议using an optional promise;但是,该代码似乎也可以让您的对象保持活力。
相反,我已经通过使用带有嵌套绑定函数的闭包来完成它。
这是我的构造函数(打字稿)......
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.
}
}
答案 6 :(得分:1)
我认为如果没有必要通知服务器取消-最好的方法是使用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);
}
}
答案 7 :(得分:0)
除了已接受的解决方案中的可取消的Promise挂钩示例外,还可以使用一个useAsyncCallback
挂钩来包装请求回调并返回可取消的Promise。这个想法是相同的,但是钩子就像常规的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
}
答案 8 :(得分:0)
使用CPromise软件包,您可以取消承诺链,包括嵌套的。它支持AbortController和生成器,以替代ECMA异步功能。使用CPromise装饰器,您可以轻松管理异步任务,使其可以取消。
装饰器用法Live Demo:
import { async, listen, cancel, timeout } from "c-promise2";
import cpFetch from "cp-fetch";
export class TestComponent extends React.Component {
state = {
text: ""
};
@timeout(5000)
@listen
@async
*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>;
}
@cancel()
componentWillUnmount() {
console.log("unmounted");
}
}
所有阶段都完全可以取消/中止。 这是与React Live Demo
一起使用的示例export class TestComponent extends React.Component {
state = {};
async componentDidMount() {
console.log("mounted");
this.controller = new CPromise.AbortController();
try {
const json = await this.myAsyncTask(
"https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
);
console.log("json:", json);
await this.myAsyncTaskWithDelay(1000, 123); // just another async task
this.setState({ text: JSON.stringify(json) });
} catch (err) {
if (CPromise.isCanceledError(err)) {
console.log("tasks terminated");
}
}
}
myAsyncTask(url) {
return CPromise.from(function* () {
const response = yield cpFetch(url); // cancellable request
return yield response.json();
}).listen(this.controller.signal);
}
myAsyncTaskWithDelay(ms, value) {
return new CPromise((resolve, reject, { onCancel }) => {
const timer = setTimeout(resolve, ms, value);
onCancel(() => {
console.log("timeout cleared");
clearTimeout(timer);
});
}).listen(this.controller.signal);
}
render() {
return (
<div>
AsyncComponent: <span>{this.state.text || "fetching..."}</span>
</div>
);
}
componentWillUnmount() {
console.log("unmounted");
this.controller.abort(); // kill all pending tasks
}
}
import React, { useEffect, useState } from "react";
import CPromise from "c-promise2";
import cpFetch from "cp-fetch";
export function TestComponent(props) {
const [text, setText] = useState("fetching...");
useEffect(() => {
console.log("mount");
// all stages here are completely cancellable
const promise = cpFetch(props.url)
.then(function* (response) {
const json = yield response.json();
setText(`Delay for 2000ms...`);
yield CPromise.delay(2000);
setText(`Success: ${JSON.stringify(json)}`);
})
.canceled()
.catch((err) => {
setText(`Failed: ${err}`);
});
return () => {
console.log("unmount");
promise.cancel();
};
}, [props.url]);
return <p>{text}</p>;
}
答案 9 :(得分:0)
另一种替代方法是将异步函数包装在一个包装器中,该包装器将在组件卸载时处理用例
正如我们所知,函数也是 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 :(得分:0)
只需四步:
1.创建 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()
}
答案 11 :(得分:-1)
我想我找到了解决方法。问题不在于获取本身,而是在组件被解除后的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,
})
}