reactjs 如何取消对 componentWillUnmount 的提取
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/49906437/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to cancel a fetch on componentWillUnmount
提问by Jo?o Belo
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(orforceUpdate) on an unmounted component. This is a no-op, but ... To fix, cancel all subscriptions and asynchronous tasks in thecomponentWillUnmountmethod.
警告:无法在未安装的组件上调用
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);
});
}
采纳答案by Tomasz Mularczyk
When you fire a Promise it might take a few seconds before it resolves and by that time user might have navigated to another place in your app. So when Promise resolves setStateis executed on unmounted component and you get an error - just like in your case. This may also cause memory leaks.
当您触发 Promise 时,它可能需要几秒钟才能解决,届时用户可能已经导航到您应用中的另一个位置。因此,当setState在未安装的组件上执行Promise resolves 并且您收到错误时 - 就像您的情况一样。这也可能导致内存泄漏。
That's why it is best to move some of your asynchronous logic out of components.
这就是为什么最好将一些异步逻辑移出组件的原因。
Otherwise, you will need to somehow cancel your Promise. Alternatively - as a last resort technique (it's an antipattern) - you can keep a variable to check whether the component is still mounted:
否则,您将需要以某种方式取消您的 Promise。或者 - 作为最后的手段(这是一种反模式) - 您可以保留一个变量来检查组件是否仍然安装:
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 is an antipatternbut may be sufficient in your case (just like they did with Formikimplementation).
我将再次强调这一点 - 这是一种反模式,但在您的情况下可能就足够了(就像他们对Formik实现所做的那样)。
A similar discussion on GitHub
GitHub上的类似讨论
EDIT:
编辑:
This is probably how would I solve the same problem (having nothing but React) with Hooks:
这可能是我如何使用Hooks解决同样的问题(只有 React):
OPTION A:
选项 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;
}
OPTION B:Alternatively with useRefwhich behaves like a static property of a class which means it doesn't make component rerender when it's value changes:
选项 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
}
Example: https://codesandbox.io/s/86n1wq2z8
回答by haleonj
The friendly people at React recommendwrapping your fetch calls/promises in a cancelable promise. While there is no recommendation in that documentation to keep the code separate from the class or function with the fetch, this seems advisable because other classes and functions are likely to need this functionality, code duplication is an anti-pattern, and regardless the lingering code should be disposed of or canceled in componentWillUnmount(). As per React, you can call cancel()on the wrapped promise in componentWillUnmountto avoid setting state on an unmounted component.
React 的友好人士建议将您的 fetch 调用/承诺包装在可取消的承诺中。虽然该文档中没有建议将代码与具有 fetch 的类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,代码重复是一种反模式,无论代码是否存在应在 中处理或取消componentWillUnmount()。根据 React,您可以调用cancel()包装好的 PromisecomponentWillUnmount以避免在未安装的组件上设置状态。
The provided code would look something like these code snippets if we use React as a guide:
如果我们使用 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();
}
---- EDIT ----
- - 编辑 - -
I have found the given answer may not be quite correct by following the issue on GitHub. Here is one version that I use which works for my purposes:
通过关注 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;
}
};
};
The idea was to help the garbage collector free up memory by making the function or whatever you use null.
这个想法是通过使函数或任何您使用的内容为 null 来帮助垃圾收集器释放内存。
回答by Paduado
You can use AbortControllerto cancel a fetch request.
您可以使用AbortController取消获取请求。
See also: https://www.npmjs.com/package/abortcontroller-polyfill
另见: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>
回答by Ben Yitzhaki
Since the post had been opened, an "abortable-fetch" has been added. https://developers.google.com/web/updates/2017/09/abortable-fetch
由于该帖子已被打开,因此添加了“abortable-fetch”。 https://developers.google.com/web/updates/2017/09/abortable-fetch
(from the docs:)
(来自文档:)
The controller + signal manoeuvre Meet the AbortController and AbortSignal:
控制器+信号机动 满足 AbortController 和 AbortSignal:
const controller = new AbortController();
const signal = controller.signal;
The controller only has one method:
控制器只有一种方法:
controller.abort(); When you do this, it notifies the signal:
控制器中止();当你这样做时,它会通知信号:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
This API is provided by the DOM standard, and that's the entire API. It's deliberately generic so it can be used by other web standards and JavaScript libraries.
这个 API 是由 DOM 标准提供的,这就是整个 API。它是故意通用的,因此可以被其他 Web 标准和 JavaScript 库使用。
for example, here's how you'd make a fetch timeout after 5 seconds:
例如,以下是您在 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);
});
回答by Anthony Wieser
The crux of this warning is that your component has a reference to it that is held by some outstanding callback/promise.
这个警告的关键是你的组件有一个对它的引用,它被一些未完成的回调/承诺所持有。
To avoid the antipattern of keeping your isMounted state around (which keeps your component alive) as was done in the second pattern, the react website suggests using an optional promise; however that code also appears to keep your object alive.
为了避免在第二种模式中保持你的 isMounted 状态(让你的组件保持活动状态)的反模式,react 网站建议使用可选的 promise;但是,该代码似乎也使您的对象保持活动状态。
Instead, I've done it by using a closure with a nested bound function to setState.
相反,我通过使用带有嵌套绑定函数的闭包来完成它。
Here's my constructor(typescript)…
这是我的构造函数(打字稿)...
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.
}
}
回答by Sasha Kos
I think if it is not necessary to inform server about cancellation - best approach is just to use async/await syntax (if it is available).
我认为如果没有必要通知服务器取消 - 最好的方法就是使用 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);
}
}
回答by Sasha Kos
When I need to "cancel all subscriptions and asynchronous" I usually dispatch something to redux in componentWillUnmount to inform all other subscribers and send one more request about cancellation to server if necessary
当我需要“取消所有订阅和异步”时,我通常会向 componentWillUnmount 中的 redux 发送一些内容,以通知所有其他订阅者,并在必要时再向服务器发送一个关于取消的请求
回答by Thomas Jgenti
In addition to the cancellable promise hooks examples in the accepted solution, it can be handy to have a useAsyncCallbackhook wrapping a request callback and returning a cancellable promise. The idea is the same, but with a hook working just like a regular useCallback. Here is an example of implementation:
除了已接受的解决方案中的可取消承诺挂钩示例之外,让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
}
回答by Jo?o Belo
I think I figured a way around it. The problem is not as much the fetching itself but the setState after the component is dismissed. So the solution was to set this.state.isMountedas falseand then on componentWillMountchange it to true, and in componentWillUnmountset to false again. Then just if(this.state.isMounted)the setState inside the fetch. Like so:
我想我找到了解决方法。问题不在于获取本身,而在于解除组件后的 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,
})
}

