reactjsasync-awaituse-effect

Are async useEffect Callbacks Actually Harmful, or Just a "Smell"?


I can find lots of articles on the web that will tell you to change:

 useEffect(async() => {
   await doSomethingAsynchronous();
 })

into:

 useEffect(() => {
   const pointlessFunction = async () => {
       await doSomethingAsynchronous();
   };
   pointlessFunction();
 })

But what I can't find is any explanation of the actual harm caused by the former.

I understand that React's ESLint rules will complain if you do the former, and I understand that it's complaining because the former might confusingly suggest that useEffect is waiting for the AJAX to complete (when it's not).

In both cases the async function is generating a promise, and in both cases that promise is being ignored (meaning that neither version is actually waiting for doSomethingAsynchronous to complete).

But ... if you understand the above, and can still write working code in spite of it ... is there any actual harm to doing the former?

EDIT:

It seems from the answers so far there is some "harm", but it's only in the form of limiting options. If I go with form #2, I can't:

So maybe that's the only harm: form #1 can't error handle or do cleanup. But I'm still curious: aside from limiting my options, if I don't need to cleanup or handle errors, will form #1 actually break anything (ie. cause harm)?

After all, using a one-line arrow function (() => 5) limits your options too ... but we still use them all the time, and just use the more verbose form when we need it. Why not do the same here?


Solution

  • While the other answers here are great and informative, none directly answered my original question, ie. "Is there any harm to providing useEffect with an async callback?

    The answer is ... no, there is no harm.

    However, there is an important limitation to that approach, which is that you cannot provide a cleanup function if you use it (because async functions can never return functions, only promises). Also, some devs may be concerned that future versions of React may break when an async callback is given to useEffect.

    Both concerns can easily be addressed with a wrapper around useEffect, called something like useAsyncEffect. This allows you to avoid ES Lint issues (and ensure compatibility with future React versions), while also still providing a cleanup function.

    export const useAsyncEffect = (effect, dependencies, cleanup) => {
      // If no dependencies are provided but a cleanup function is, fix args
      if (!cleanup && typeof dependencies === 'function') {
        cleanup = dependencies;
        dependencies = undefined;
      }
    
      // Main wrapper code
      useEffect(() => {
        effect();
        return cleanup;
      }, dependencies);
    };
    

    Example Usage:

    const [foo, setFoo] = useState(null);
    useAsyncEffect(async () => {
        const response = await fetch(`www.example.com/foo?id=${someId}`);
        const foo = await response.json();
        setFoo(foo);
    }, [someId], someCleanupFunction);