Skip to content

Instantly share code, notes, and snippets.

@tomhicks
Created January 11, 2021 11:41
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save tomhicks/95d455186fadda2f7d99e6163aa9d360 to your computer and use it in GitHub Desktop.
Save tomhicks/95d455186fadda2f7d99e6163aa9d360 to your computer and use it in GitHub Desktop.
React Hook for queueing and processing async tasks sequentially
function useTaskQueue(params: {
shouldProcess: boolean
}): {
tasks: ReadonlyArray<Task>
isProcessing: boolean
addTask: (task: Task) => void
} {
const [queue, setQueue] = React.useState<{
isProcessing: boolean
tasks: Array<Task>
}>({isProcessing: false, tasks: []})
React.useEffect(() => {
if (!params.shouldProcess) return
if (queue.tasks.length === 0) return
if (queue.isProcessing) return
const task = queue.tasks[0]
setQueue((prev) => ({
isProcessing: true,
tasks: prev.tasks.slice(1),
}))
Promise.resolve(task()).finally(() => {
setQueue((prev) => ({
isProcessing: false,
tasks: prev.tasks,
}))
})
}, [queue, params.shouldProcess])
return {
tasks: queue.tasks,
isProcessing: queue.isProcessing,
addTask: React.useCallback((task) => {
setQueue((prev) => ({
isProcessing: prev.isProcessing,
tasks: [...prev.tasks, task],
}))
}, []),
}
}
type Task = () => Promise<void> | void
@sagaban
Copy link

sagaban commented Nov 4, 2021

Thanks a lot!!

@diidiiman
Copy link

Thank you! This was the architecture I was looking for to implement queue processing in the background.
Using this to make image upload to AWS in the non-block blocking way for the end user. Thanks for sharing this!

@jimmy010679
Copy link

Thanks ~

@sladkoff
Copy link

sladkoff commented Apr 7, 2023

I started getting `Maximum update depth exceeded' errors since upgrading to React 18.

After some debugging and research, I could pin the cause on the new automatic batching in React 18.

In this hook, there are two consecutive setQueue(...) calls inside the useEffect which must update the internal state in the correct order. Otherwise there's the possibility for inifinite re-renders which cause the above error. This only happens if the enqueued task() is very fast.

This can be fixed by wrapping the first setQueue call in flushSync to prevent batching:

flushSync(() => {
    setQueue((prev) => ({
      isProcessing: true,
      tasks: prev.tasks.slice(1),
    }))
})

As the use of flushSync is discouraged, I'd also be interested in a different solution.

PS. Thanks for the original hook!

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