如何正确清理包含异步 GraphQL 操作的 useEffect。

huangapple go评论81阅读模式
英文:

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

以下是您要翻译的内容:

  1. If getFirstWorkspace offers a way to tell it to cancel what it's doing, you'd do that. For instance, if it supported AbortSignal, 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.

  2. 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. 如何正确清理包含异步 GraphQL 操作的 useEffect。

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:

  1. If getFirstWorkspace offers a way to tell it to cancel what it's doing, you'd do that. For instance, if it supported AbortSignal, 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.

  2. 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. 如何正确清理包含异步 GraphQL 操作的 useEffect。


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

huangapple
  • 本文由 发表于 2023年2月8日 17:11:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/75383462.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定