Modal Service
A window of content placed on top of the viewport, rendering the content underneath inert.
See useModal for full documentation on hook usage.
Doing something different
Problem
Modals are commonly written as components. Rendering these modals 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 modal'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 modalService
aims to solve these problems by providing tooling that allows
them to be extremely reusable with minimal boilerplate. Opening a modal is as
easy as calling a function. The modal opens in a DrawerEntry
component
rendered in the root of your application.
The ultimate goal is ease of reusability. Writing your modal in a custom hook affords all of the above:
// abstraction
function useCustomModal() {
return useModal(() => <div>Hello, World!</div>);
}
// implementation
function ParentComponent() {
const myModal = useCustomModal();
return <Button onClick={() => myModal()}>Open modal</Button>;
}
Promise-based API
The modalService
handles opening/closing with promises. This means that when
you open a modal, you can await its result. The state of when a modal closes is
completely in the hands of the modal itself.
In this example, the modal is a yes/no confirmation:
function useConfirmModal() {
return useModal(() => {
const { closeModal } = useModalContext();
return (
<>
<p>Are you sure?</p>
<button onClick={() => closeModal(true)}>Yes</button>
<button onClick={() => closeModal(false)}>No</button>
</>
);
});
}
We can now await the response of the modal - no state changes, no callbacks,... just a simple await:
function MyComponent() {
const myModal = useConfirmModal();
async function openModal() {
if (await myModal()) {
console.log("Confirmed");
} else {
console.log("Denied");
}
}
return <button onClick={openModal}>Open Modal</button>;
}
This does 2 things:
- It separates the modal 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 modals need to be rendered outside a component. The useModal
hook is
just a wrapper around the modalService.open
function.
const result = await modalService.open(() => <div>Hello, World!</div>);
Internally, the service will keep track of this modal with a dynamically
generated id
. In some rare cases, you might want to force close the modal 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
modalService.open(() => <div>Hello, World!</div>, undefined, { id });
// close
modalService.close(id);
Methods
open
open<T>(
ModalComponent: React.FC<T>,
props?: T,
options: {
id?: string;
size?: "sm" | "md" | "lg";
} = {},
): Promise
Open a modal using a passed-in component, props, and options. A promise is returned that resolves when the modal closes.
function MyComponent({ name }: { name: string }) {
return <button onClick={myModal}>Open Modal</button>;
}
const result = await modalService.open(
MyComponent,
{ name: "John Doe" },
{ size: "sm", id: "custom-id" },
);
close
close(id: string, result: unknown): void
Manually close the modal from the parent. Requires the id
of the modal to
close.
modalService.close("custom-id", "result");
closeAll
closeAll(): void
Close all open modals.
modalService.closeAll();