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()returnstrueif the component is still mounted andfalseotherwise. 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
- Effect Initialization: When the hook runs (on mount or dependency change), it initializes a
mountedflag totrue. - Execution: It calls your
effectfunction, passing a closure that reads themountedflag. - Promise Handling: It wraps the result in
Promise.resolve()to ensure it handles both synchronous and asynchronous returns gracefully. - Result Capture: The resolved value of your effect is stored in a local variable within the
useEffectclosure. - Unmounting / Re-running: When the component unmounts or dependencies change:
- The
mountedflag is set tofalse. Any subsequent calls toisMounted()within your async function will now returnfalse. - The
destroycallback (if provided) is executed, receiving the captured result of the previous effect.
- The
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>;
}