Dialog Service
A window of content placed on top of the viewport, rendering the content underneath inert.
See useDialog for full documentation on hook usage.
Doing something different
Problem
Dialogs/modals are commonly written as components. Rendering these dialogs
typically involves managing their "display" state in the parent component and
passing callbacks like onSubmit
to it. While this works, the parent component
must manage the dialog's state and behavior, which results in duplicated logic
across all consumers of it. Even if you write an abstraction for this
functionality, this state management is still required for all consumers... and
it's cumbersome.
Solution
The dialogService
aims to solve these problems by providing tooling that
allows them to be extremely reusable with minimal boilerplate. Opening a dialog
is as easy as calling a function. The dialog opens in a DialogEntry
component
rendered in the root of your application.
The ultimate goal is ease of reusability. Writing your dialog in a custom hook affords all of the above:
// abstraction
function useCustomDialog() {
return useDialog(() => <div>Hello, World!</div>);
}
// implementation
function ParentComponent() {
const myDialog = useCustomDialog();
return <Button onClick={() => myDialog()}>Open dialog</Button>;
}
Promise-based API
The dialogService
handles opening/closing with promises. This means that when
you open a dialog, you can await its result. The state of when a dialog closes
is completely in the hands of the dialog itself.
In this example, the dialog is a yes/no confirmation:
function useConfirmDialog() {
return useDialog(() => {
const { closeDialog } = useDialogContext();
return (
<>
<p>Are you sure?</p>
<button onClick={() => closeDialog(true)}>Yes</button>
<button onClick={() => closeDialog(false)}>No</button>
</>
);
});
}
We can now await the response of the dialog - no state changes, no callbacks,... just a simple await:
function MyComponent() {
const myDialog = useConfirmDialog();
async function openDialog() {
if (await myDialog()) {
console.log("Confirmed");
} else {
console.log("Denied");
}
}
return <button onClick={openDialog}>Open Dialog</button>;
}
This does 2 things:
- It separates the dialog into its own component for easy reusability.
- It pulls all state and callback management out of the parent component. The parent component has no idea what the child component does, except what it should output.
Outside a component
Sometimes dialogs need to be rendered outside a component. The useDialog
hook
is just a wrapper around the dialogService.open
function.
const result = await dialogService.open(() => <div>Hello, World!</div>);
Internally, the service will keep track of this dialog with a dynamically
generated id
. In some rare cases, you might want to force close the dialog
from the parent which will require passing in an id
to the options
object.
Reuse this id
to close it:
const id = "super-id";
// open
dialogService.open(() => <div>Hello, World!</div>, undefined, { id });
// close
dialogService.close(id);
Methods
open
open<T>(
DialogComponent: React.FC<T>,
props?: T,
options: {
id?: string;
size?: "sm" | "md" | "lg";
} = {},
): Promise
Open a dialog using a passed-in component, props, and options. A promise is returned that resolves when the dialog closes.
function MyComponent({ name }: { name: string }) {
return <button onClick={myDialog}>Open Dialog</button>;
}
const result = await dialogService.open(
MyComponent,
{ name: "John Doe" },
{ size: "sm", id: "custom-id" },
);
close
close(id: string, result: unknown): void
Manually close the dialog from the parent. Requires the id
of the dialog to
close.
dialogService.close("custom-id", "result");
closeAll
closeAll(): void
Close all open dialogs.
dialogService.closeAll();