Skip to main content

useAsyncEffect Deep Dive

The useAsyncEffect hook is a specialized version of React's useEffect that natively supports asynchronous functions and provides a mechanism to track the component's mount status.


Asynchronous Actions

Standard useEffect cannot accept an async function because it expects the return value to be either void or a cleanup function. useAsyncEffect solves this by handling the promise internally.

useAsyncEffect(async (isMounted) => {
const result = await api.fetchData();
if (isMounted()) {
setData(result);
}
}, [deps]);

Mount Tracking

The most powerful feature of useAsyncEffect is the isMounted callback passed as the first argument to your effect.

  • Why?: In asynchronous operations, the component might unmount before the promise resolves. Updating state on an unmounted component can lead to memory leaks and errors.
  • How?: isMounted() returns true if the component is still mounted and false otherwise. Always check this before updating state.

Cleanup Strategy

If you need to perform a cleanup operation (similar to the function returned by useEffect), you can provide an AsyncOptions object:

useAsyncEffect({
effect: async (isMounted) => {
const socket = await connect();
return socket; // Value returned here is passed to destroy
},
destroy: (socket) => {
socket.disconnect();
}
}, []);

How it Works Step-by-Step

  1. Effect Initialization: When the hook runs (on mount or dependency change), it initializes a mounted flag to true.
  2. Execution: It calls your effect function, passing a closure that reads the mounted flag.
  3. Promise Handling: It wraps the result in Promise.resolve() to ensure it handles both synchronous and asynchronous returns gracefully.
  4. Result Capture: The resolved value of your effect is stored in a local variable within the useEffect closure.
  5. Unmounting / Re-running: When the component unmounts or dependencies change:
    • The mounted flag is set to false. Any subsequent calls to isMounted() within your async function will now return false.
    • The destroy callback (if provided) is executed, receiving the captured result of the previous effect.

Best Practice: Always utilize the isMounted() check after any await keyword to ensure your component logic remains safe and doesn't attempt to interact with a destroyed DOM or state.


Full Example

Basic Async Action

import { useAsyncEffect } from '@archibald/client';

function TestComponent() {
useAsyncEffect(async (isMounted) => {
const data = await fetchData();
if (isMounted()) {
setData(data);
}
}, []);

return <div>Test</div>;
}

With Cleanup (Destroy)

import { useAsyncEffect } from '@archibald/client';

function TestComponent() {
useAsyncEffect({
effect: async (isMounted) => {
const timer = await startTimer();
return timer;
},
destroy: (timer) => {
stopTimer(timer);
}
}, []);

return <div>Test</div>;
}