英文:
how to properly cleanup useEffect that contain async graphql operation
问题
我正在使用graphql/apollo和react。
我有以下代码
const [state, setState] = useState(undefined);
useEffect(() => {
(async () => {
try {
const workspace = await getFirstWorkspace();
// 做某事
setState(withSomething)
} catch (error) {
// 做其他事情
setState(withErrorSomething)
}
})();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
现在,这段代码在我更新了一些包之前工作正常,但是现在出现了以下错误。
Uncaught (in promise) DOMException: signal is aborted without reason
据我理解,我的useEffect
在组件卸载时抛出此错误,并且查询没有完成运行。
现在,这会导致catch
至少触发一次,因为似乎在再次运行useEffect
时,其中一个依赖项更改,它会失败。
我通过以下方式“修复”了它:
const [state, setState] = useState(undefined);
useEffect(() => {
(async () => {
try {
const workspace = await getFirstWorkspace();
// 做某事
setState(withSomething)
} catch (error) {
// 做其他事情
if ((error as any)?.name === 'AbortError') {
return;
}
setState(withErrorSomething)
}
})();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
并且在错误是中止时不分配任何状态。但我找不到任何合适的解决方案,或者我不明白为什么这在以前没有问题,而现在有问题,我确实更新了一些包,但没有提到此方面行为的更改。
我的问题是,我应该怎么做才能正确地处理这种情况?
英文:
I am using graphql/apollo and react.
I have the following code
const [state, setState] = useState(undefined);
useEffect(() => {
(async () => {
try {
const workspace = await getFirstWorkspace();
// Do Something
setState(withSomething)
} catch (error) {
// Do Something Else
setState(withErrorSomething)
}
})();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
now, this worked fine until I updated some packages, I currently get thrown this error.
> Uncaught (in promise) DOMException: signal is aborted without reason
From what I understand my useEffect throw this when the component is unmounted an the query didn't finish to run.
Now, this cause my catch to always trigger at least once, cause it looks like when the effect is run again cause one of the dep changed, it fail.
I """ fixed """ it by doing
const [state, setState] = useState(undefined);
useEffect(() => {
(async () => {
try {
const workspace = await getFirstWorkspace();
// Do Something
setState(withSomething)
} catch (error) {
// Do Something Else
if ((error as any)?.name === 'AbortError') {
return;
}
setState(withErrorSomething)
}
})();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
And not assign any state in case the error is an abort. But I couldn't find any proper solution or I don't understand why this is problematic before and not now, I did update some package but none mention a change of behavior on this end.
My question is, what should I do to do thing correctly ?
答案1
得分: 2
以下是您要翻译的内容:
-
If
getFirstWorkspace
offers a way to tell it to cancel what it's doing, you'd do that. For instance, if it supportedAbortSignal
, you might do this:useEffect(() => { // Create a controller and get its signal const controller = new AbortController(); const { signal } = controller; (async () => { try { // Pass the signal to `getFirstWorkspace` const workspace = await getFirstWorkspace(signal); // Only do something if the signal isn't aborted if (!signal.aborted) { // Do Something setState(withSomething); } } catch (error) { // Only do something if the signal isn't aborted if (!signal.aborted) { // Do Something Else setState(withErrorSomething); } } })(); return () => { // Abort the signal on cleanup controller.abort(); }; }, [generateLink, getFirstWorkspace, masterDataStoreId]);
...or similar if it doesn't support
AbortSignal
specifically but does provide some other way of canceling its work. -
If it doesn't, you could fall back to a flag telling you not to use the result:
useEffect(() => { // Start with a flag set to `false` let cancelled = false; (async () => { try { const workspace = await getFirstWorkspace(); // Only do something if the flag is still `false` if (!cancelled) { // Do Something setState(withSomething); } } catch (error) { // Only do something if the flag is still `false` if (!cancelled) { // Do Something Else setState(withErrorSomething); } } })(); return () => { // Set the flag on cleanup cancelled = true; }; }, [generateLink, getFirstWorkspace, masterDataStoreId]);
It's better to actually cancel the work if you can, but it's fine to have a fallback boolean if you can't. Just don't assume you can't, be sure to check first.
Side note: I love async
/await
, but when you're doing just a single call and getting a promise, doing an async
wrapper and try
/catch
around await
can be a bit overkill. FWIW, just using the promise directly looks like this (using the flag in this case, but it works just as well with the controller/signal):
useEffect(() => {
let cancelled = false;
getFirstWorkspace().then(
(workspace) => {
if (!cancelled) {
// Do Something
setState(withSomething);
}
},
(error) => {
if (!cancelled) {
// Do Something Else
setState(withErrorSomething);
}
}
);
return () => {
cancelled = true;
};
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
英文:
I don't think the error you've quoted is coming from React. React used to complain if you did a state update in a component that was no longer mounted, but the error message it used was "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." But recent versions of React don't do that because the React team decided it was too fussy.
Still, answering the question as asked:
-
If
getFirstWorkspace
offers a way to tell it to cancel what it's doing, you'd do that. For instance, if it supportedAbortSignal
, you might do this:useEffect(() => { // *** Create a controller and get its signal const controller = new AbortController(); const { signal } = controller; (async () => { try { // *** Pass the signal to `getFirstWorkspace` const workspace = await getFirstWorkspace(signal); // *** Only do something if the signal isn't aborted if (!signal.aborted) { // Do Something setState(withSomething); } } catch (error) { // *** Only do something if the signal isn't aborted if (!signal.aborted) { // Do Something Else setState(withErrorSomething); } } })(); return () => { // *** Abort the signal on cleanup controller.abort(); }; }, [generateLink, getFirstWorkspace, masterDataStoreId]);
...or similar if it doesn't support
AbortSignal
specifically but does provide some other way of cancelling its work. -
If it doesn't, you could fall back to a flag telling you not to use the result:
useEffect(() => { // *** Start with a flag set to `false` let cancelled = false; (async () => { try { const workspace = await getFirstWorkspace(); // *** Only do something if the flag is still `false` if (!cancelled) { // Do Something setState(withSomething); } } catch (error) { // *** Only do something if the flag is still `false` if (!cancelled) { // Do Something Else setState(withErrorSomething); } } })(); return () => { // *** Set the flag on cleanup cancelled = true; }; }, [generateLink, getFirstWorkspace, masterDataStoreId]);
It's better to actually cancel the work if you can, but it's fine to have a fallback boolean if you can't. Just don't assume you can't, be sure to check first.
Side note: I love async
/await
, but when you're doing just a single call and getting a promise, doing an async
wrapper and try
/catch
around await
can be a bit overkill. FWIW, just using the promise directly looks like this (using the flag in this case, but it works just as well with the controller/signal):
useEffect(() => {
let cancelled = false;
getFirstWorkspace().then(
(workspace) => {
if (!cancelled) {
// Do Something
setState(withSomething);
}
},
(error) => {
if (!cancelled) {
// Do Something Else
setState(withErrorSomething);
}
}
);
return () => {
cancelled = true;
};
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
答案2
得分: 0
You could make use of AbortController.
const [state, setState] = useState(undefined);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
(async () => {
try {
const workspace = await getFirstWorkspace(signal);
// 做一些操作
setState(withSomething);
} catch (error) {
// 做其他操作
setState(withErrorSomething);
}
})();
return () => controller.abort();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
英文:
U could make use of AbortController
const [state, setState] = useState(undefined);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
(async () => {
try {
const workspace = await getFirstWorkspace(signal);
// Do Something
setState(withSomething)
} catch (error) {
// Do Something Else
setState(withErrorSomething)
}
})();
return =()=>Controller.abort();
}, [generateLink, getFirstWorkspace, masterDataStoreId]);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论