Blog post
Managing background tasks with a React context provider
A proof of concept showing how to coordinate long-running work in React with a BackgroundTaskProvider and a custom useBackgroundTasks hook.
Intro
I built a small proof of concept to explore how background tasks can be managed in a React app without prop drilling or external state management libraries. The idea was to create a BackgroundTaskProvider that exposes a simple API for starting, pausing, resuming, and cancelling tasks, along with a hook for components to interact with the task system.
The code for the proof of concept is on GitHub: wardpieters/react-background-task-provider-example.
What the demo does
The demo keeps task state inside a React context so any component wrapped by the provider can interact with the task system.
Each task tracks a few basic fields:
progressisPausedisCompletedisErrorisCancellederror
The sample app starts a simulated long-running job that increments progress in steps. While it runs, the UI can pause, resume, or cancel the task, and the current task list is rendered directly from context state.
Provider design
The core of the proof of concept is the BackgroundTaskProvider component. It stores the visible task list in React state and also keeps a mutable ref for fast task lookups and updates.
That split is intentional:
- React state drives re-rendering of the UI.
- The ref acts as the source of truth for task control flow inside async task runners.
When startTask is called, the provider creates a new task ID, stores an initial status object, and immediately starts the supplied async function. The runner receives a small control API:
getStatus()to inspect the current task state.setStatus()to update fields like progress.shouldPause()to wait while the task is paused.shouldCancel()to stop execution if the task has been cancelled.
That gives the task function enough information to cooperate with the provider without knowing anything about the surrounding UI.
The hook interface
The useBackgroundTasks hook is intentionally thin. It just reads the context and throws an error if used outside the provider.
That keeps consumer code simple while still making the dependency explicit. Components either live inside the provider and can control tasks, or they fail fast during development.
How pause and cancel work
Pause is cooperative. The runner checks shouldPause() and waits in a short loop until the task is resumed.
Cancel is also cooperative, but stricter. The task checks shouldCancel() and throws if the task has been marked as cancelled or removed.
That approach is enough for a proof of concept because it works cleanly with async JavaScript tasks. It also makes the tradeoff obvious: the task must periodically check in with the provider for cancellation and pause state.
Why not use workers?
Both Web Workers and service workers can communicate with the UI in both directions, so in theory pause actions could be sent from React and progress updates could be posted back.
I still chose not to use either one for this prototype. The main goal was to keep progress and control state easy to surface in the UI, and that is simpler when the task stays close to the React tree. Workers add message passing, serialization overhead, and a second state channel to keep in sync, which makes pause, cancel, and incremental progress updates harder to reason about for a small demo like this. Service workers also have a different lifecycle and are aimed at network and caching work, so they are not a natural fit for a UI-driven task runner.
What I learned
This prototype confirmed that React context is a reasonable fit for task orchestration when the task lifecycle is mostly UI-driven.
The main benefits are simplicity and discoverability. Any component can start or control a task through the same hook, and the UI can render directly from a shared task list. The provider encapsulates the task logic and state management, so components don't need to worry about the details of how tasks are tracked or updated.
Conclusion
The proof of concept showed that a BackgroundTaskProvider plus a useBackgroundTasks hook can form a clean, testable interface for background work in React.
If I turn this into a production feature, the next steps would be better task history and stronger error handling. But as a starting point, the architecture already makes the control flow easy to understand.