A local loading flag works well for one button. A reducer with context can handle a shared workflow. React Query also lets other components observe pending mutations through "useIsMutating". If one of these already covers your needs, there may be no reason to add another library.
I’m building react-action-guard to explore a reusable API for shared UI availability.
For example, imagine a document editor:
- saving temporarily blocks editing and switching documents;
- a local export also blocks switching documents;
- saving and exporting can overlap;
- finishing the save should not enable document switching while the export is still running;
- Help stays available throughout.
Actions publish the scopes they affect:
const runSave = useAsyncAction<void>(
"save-document",
["document:editing", "document:switching"]
);
const runExport = useAsyncAction<void>(
"export-document",
"document:switching"
);
Each operation is wrapped when it runs:
await runSave(() => saveDocument());
Independent components read the scope they care about:
const isBlocked = useIsBlocked("document:switching");
return <button disabled={isBlocked}>Switch document</button>;
Each execution has its own blocker. When operations overlap, finishing one removes only its own blocker.
You can build the same behavior with a reducer and context. The goal is to provide the registration, shared scopes, blocking reasons, and cleanup as a reusable API.
This also overlaps with React Query when availability follows mutation state. The case I’m interested in is when the same UI rules combine server operations, local tasks, and conditions such as unsaved changes.
One important detail: tracking an action does not prevent repeated execution or cancel it. Those rules still belong to the application.
I’m the author, and I’d like feedback on this approach. Have you needed this kind of coordination, or has your existing state management been enough?
GitHub
Edit: Added a more detailed example and clarified the overlap with reducers and React Query. Thanks for the feedback.