You can use this custom hook:
import { useEffect, useRef } from "react";
import lodash from "lodash";
export const useEffectAsync = (
func: () => Promise<any>,
dependencies: any[]
) => {
let tasks = useRef<{ func: typeof func }[]>([]);
const runWaitingTasks = () => {
if (tasks.current.length) {
tasks.current[0].func().then(() => {
let tasksCopy = lodash.cloneDeep(tasks.current);
tasksCopy.splice(0, 1);
tasks.current = tasksCopy;
runWaitingTasks();
});
}
};
useEffect(() => {
tasks.current.push({ func });
if (tasks.current.length === 1) {
runWaitingTasks();
}
}, dependencies);
};
This hook is just created by combining the basic useEffect with a queue to manage dependencies changes asynchronously.
Simple example:
import { useState } from "react";
import { useEffectAsync ,anApiCallAsync} from "./Utils";
function App() {
useEffectAsync(async () => {
let response = await anApiCallAsync();
console.log(response)
}, []);
return (
<div>
<h1>useEffectAsync!</h1>
</div>
);
}
export default App;
Description:
Consider this example:
import { useState } from "react";
import { useEffectAsync } from "./Utils";
function App() {
const [counter, setCounter] = useState(0);
const sleep = (sleep: number) =>
new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
}, sleep);
});
useEffectAsync(async () => {
await sleep(1500);
console.log("useEffectAsync task with delay, counter: " + counter);
}, [counter]);
return (
<div>
<button onClick={() => setCounter(counter + 1)}>click to increase</button>
<div>{counter}</div>
</div>
);
}
export default App;
In the above example when the counter is updated, the useEffectAsync hook ensures that the previous task finishes before executing the inner function with the new counter value.
Actually, if a task is currently running and the dependencies change at the same time, this custom hook effectively creates a queue for incoming tasks and executes sequentially, one after the other, with new dependency values.
This can be particularly useful for scenarios where you have time-consuming tasks and you need to ensure they complete one after the other with updated dependencies.
If you run the above example via useEffect and use the async function inside of it, every time dependencies change,
the useEffect runs the inner function instantly without waiting for previous tasks to finish, potentially causing concurrency issues.
Below is the sample code with useEffect:
import { useEffect, useState } from "react";
function App() {
const [counter, setCounter] = useState(0);
const sleep = (sleep: number) =>
new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
}, sleep);
});
useEffect(() => {
(async () => {
await sleep(1500);
console.log("useEffect task with delay, counter: " + counter);
})();
}, [counter]);
return (
<div>
<button onClick={() => setCounter(counter + 1)}>click to increase</button>
<div>{counter}</div>
</div>
);
}
export default App;
I hope this custom hook is helpful for managing async tasks in your React components.