Skip to content

Instantly share code, notes, and snippets.

@jcary741
Created June 13, 2023 15:03
Show Gist options
  • Save jcary741/cee7cb3c2d42be1bb9f834b120894187 to your computer and use it in GitHub Desktop.
Save jcary741/cee7cb3c2d42be1bb9f834b120894187 to your computer and use it in GitHub Desktop.
useEffectAsync: a cancellable and async version of the React useEffect hook
import {useEffect} from "react";
/**
* A hook that runs an async function on mount with support for cancellation.
*
* If unmounting before the async function completes, the cleanup function will be called.
* @param fn {function} A function that takes a 'cancelled' function as its only argument.
* @param inputs {array?} An optional array of inputs to pass to useEffect.
* @param cleanup {function?} An optional cleanup function to run on unmount.
*
* @example
* useEffectAsync(async (cancelled) => {
* const result = await someAsyncFunction();
* if (!cancelled()) {
* // do something with result
* }
* });
*
* @example
* useEffectAsync(async (cancelled) => {
* if (someValue == 'something') {
* const result = await someAsyncFunction();
* if (cancelled()) return;
* setSomeState(result);
* }
* }, [someValue, setSomeState]);
*
*/
export const useEffectAsync = (fn, inputs=null, cleanup=null,) => {
let cancelled = false;
useEffect(() => {
fn(()=>cancelled);
return () => {
cancelled = true;
if (cleanup) cleanup();
}
}, inputs);
};
/**
* Simple sleep function for use with async/await.
*
* @param ms {number}
* @returns {Promise<null>}
*/
export async function sleep(ms) {
return new Promise((resolve) =>setTimeout(()=>(resolve()), ms));
}
@jcary741
Copy link
Author

jcary741 commented Jun 13, 2023

I had previous received the console error "Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async() => ...) are not supported, but you can call an async function inside an effect" and switched to doing just that, immediately invoking an async inside my useEffect hook like this:

useEffect(()=>{
  (async ()=>{
    // my code
  })();
});

but it was cumbersome, and I occasionally encountered race conditions where the hook was invoked twice for non-idempotent functions. So, I came up with this simple cancellable solution that still honors the contract of returning a cleanup function to React useEffect.

If you want something to be cancellable, just check after each use of await. You can do it in one line like this:

const result = await fetchMyThing(someValue);
if (cancelled()) return;
setThing(result);

Or, you could save the result on a ref and check if a previous invocation already retrieved it like this:

const thingCacheRef = useRef({});
const [thing, setThing] = useState(null);

useEffectAsync(async (cancelled) => {
  if (thingCacheRef?.current?.someValue){
    return thingCacheRef.current.someValue;
  }
  thingCacheRef.current.someValue = await fetchMyThing(someValue);
  if (cancelled()) return;
  setThing(thingCacheRef.current.someValue);
  }
}, [setThing, thingCacheRef]);

Bonus included! sleep function await sleep(30) instead of using setTimeout directly.

Hope you enjoy!

If you end up creating a TS version, please post it back here, thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment