React dialogs often start simple.
You add an isOpen state, then a selected item state, then confirm/cancel callbacks, then another dialog after the first one.
Eventually, a simple flow can become scattered across multiple components.
For example, a user flow like this:
- Select a user
- Confirm the action
- Add the user
often becomes multiple pieces of state:
const [isUserSearchOpen, setIsUserSearchOpen] = useState(false);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
This works, but the actual flow is harder to read.
What if dialogs could be handled as async flows?
I wanted the code to read closer to the user flow:
const user = await openAsync(UserSearchDialog);
if (!user) return;
const confirmed = await openAsync(ConfirmDialog, {
title: `Add ${user.name}?`,
});
if (confirmed) {
await addUser(user.id);
}
The dialog opens, waits for a result, and the caller continues based on that result.
This is about orchestration, not UI
This is not meant to replace Radix, MUI, Headless UI, shadcn/ui, or custom dialog components.
Those libraries solve the dialog UI problem well.
The idea here is to manage the flow around dialogs:
- opening dialogs from anywhere under a provider
- resolving typed result values
- handling nested dialogs
- distinguishing completed vs dismissed
- supporting dismissal reasons
- guarding close behavior with
shouldClose
So the actual dialog UI can still be your own component.
I packaged the pattern
I turned this idea into a small open-source library called react-dialog-flow.
It provides a headless dialog stack, Promise-based openAsync, typed results, nested dialogs, closeTop, closeAll, dismissal reasons, shouldClose, and optional UI primitives.
GitHub: https://github.com/CHOKANGYEOL/react-dialog-flow
npm: https://www.npmjs.com/package/react-dialog-flow
Docs: https://dialog-flow.kangyeol.com/
It is still early, so I am mainly looking for feedback on the API design.
Would you use async/await for dialog flows in React, or would you keep this logic inside app-specific hooks?
Top comments (0)