react-call: Awaiting React Components Like Async Functions
react-call is a sub-1KB, zero-dependency React library that allows interactive UI elements (modals, dialogs, toasts) to be triggered imperatively and awaited like async functions. Instead of managing visibility state and callback handlers, components wrapped with createCallable can be called with .call() and their result awaited directly in async event handlers.
The traditional approach and its friction
A typical confirmation modal requires at least one visibility state variable, one variable for contextual data, and multiple handler functions per modal:
When the triggering component is deep in the tree and the modal renders at the top, passing state and handlers down requires either prop drilling through unrelated components or a Context provider with its own boilerplate.
The react-call approach
react-call draws on the mental model of window.confirm(): execution pauses, the user responds, and the result is returned. The same flow with a custom styled component:
All state management for visibility and promise resolution is internal to the library.
Three-step setup
Declare. Wrap the component with createCallable. This returns a callable version of the component.
Root. Render the callable component once in the tree (typically in App.tsx). This instance acts as a mounting point. It renders nothing until called.
Call & Await. From anywhere in the application, trigger the component with .call() in an async function and await the result.
Core concepts
Root and Stack
Each createCallable component has a Root and an internal Stack. When .call() is triggered, the Root renders an instance of the component. If .call() is triggered again before the first instance resolves, the new instance is pushed onto the stack and rendered on top. Resolved instances are removed. This allows nested dialogs (a confirmation opening another confirmation) where each instance is isolated and manages its own promise.
Upsert
Some elements should only ever have one visible instance: toasts, progress overlays, "saving..." indicators. For these, .upsert() behaves like .call() on the first invocation but updates the props of the existing instance on subsequent calls instead of creating a new one. A file upload toast that needs to reflect changing progress can call upsert repeatedly with the new percentage, and the same component instance re-renders in place.
useMutationFlow
For dialogs that trigger async actions, useMutationFlow wires the callable component to an async function and manages the lifecycle:
- Tracks the pending state automatically
- Closes the dialog on success
- Keeps it open on failure, preserving any user-entered data for retry
This "stay-open-on-failure" guarantee is important for forms and actions where the user needs to correct and resubmit.
Complete example: confirmation dialog
Declare the callable component
call.end() resolves the promise with the provided value. Passing true or false here is what the await Confirm.call(...) expression receives.
Mount the Root
<Confirm /> renders nothing until called. It only needs to appear once in the tree.
Call and await from a consumer
The modal's visibility state, promise resolution, and cleanup are all managed by react-call. The component's event handler contains only the business logic.
Final thoughts
react-call works best for interactive elements where the result is needed in the same scope that triggered them. Confirmation dialogs, file pickers, and form-based modals fit this pattern well. The Upsert pattern is the right tool when you need singleton UI elements like toasts or progress indicators that update rather than stack.
The library's constraint is that it requires components to be wrapped at definition time and requires a Root to be mounted. This is a minor upfront cost for a significant reduction in per-modal boilerplate as an application grows.
Source code and documentation are at github.com/desko27/react-call.