Last active
January 23, 2023 05:29
-
-
Save nicksheffield/cdd6de59592bda6c41cda286549d905d to your computer and use it in GitHub Desktop.
react awaitable imperative modal paradigm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react' | |
export type ModalResolveFn<T = unknown> = (answer?: T) => void | |
// the shape of a modal definition. A unique id and an element | |
export interface ModalDefinition<T> { | |
id: string | |
element: React.ReactNode | |
handleClose: ModalResolveFn<T> | |
} | |
interface GlobalContainer { | |
addModal: <T>(x: ModalDefinition<T>) => void | |
removeModal: (x: string) => void | |
} | |
// this object is essentially used to globally access the "setModals" function from the ModalProvider. | |
// the big assumption we have to make here is that there is one ModalProvider and one react application in this codebase... | |
// this is probably fine. | |
export const globalContainer: GlobalContainer = { | |
addModal: () => {}, | |
removeModal: () => {}, | |
} | |
export interface ModalProps<T> { | |
render: (close: ModalResolveFn<T>) => React.ReactNode | |
theme?: string | |
onClose: ModalResolveFn<T> | |
} | |
// this function is exported, and is used throughout our app to show modals | |
export const modal = <T,>(def: Omit<ModalProps<T>, 'onClose'>): Promise<T | undefined> => { | |
let id = crypto.randomUUID() | |
// this is the magic: | |
// return a promise that is resolved by the "handleClose" function. | |
// this lets us use the close fn to pass data out of the modal when we close it | |
return new Promise((resolve) => { | |
// this function takes an optional argument and resolves the promise with it | |
const handleClose: ModalResolveFn<T> = (x?: T) => { | |
globalContainer.removeModal(id) | |
resolve(x) | |
} | |
// run the render function from the modal definition, providing it with the closer fn | |
const element = def.render(handleClose) | |
// add this modal to the provider state | |
globalContainer.addModal<T>({ | |
id, | |
element, | |
handleClose, | |
}) | |
}) | |
} | |
export const ModalProvider = ({ children }) => { | |
// the list of modals is housed as react state in the provider | |
const [modals, setModals] = React.useState<ModalDefinition<any>[]>([]) | |
// upon mounting of the provider | |
React.useEffect(() => { | |
// add the def to the provider state | |
const addModal = <T,>(def: ModalDefinition<T>) => setModals((defs) => [...defs, def]) | |
// remove the def from the provider state | |
const removeModal = (id: string) => setModals((defs) => defs.filter((x) => x.id !== id)) | |
// redefine the globalContainer fns: | |
globalContainer.addModal = addModal | |
globalContainer.removeModal = removeModal | |
}, []) | |
// below we render the app, and then our list of modals | |
return ( | |
<> | |
{children} | |
<div id="modals"> | |
{modals.map(modal => ( | |
<div key={modal.id} className="modal">{modal.element}</div> | |
))} | |
</div> | |
</> | |
) | |
} | |
/// example usage: | |
const SomeComponent = () => { | |
const openMyModal = () => modal<boolean>({ | |
render: (close) => ( | |
<div> | |
<div>Are you sure?</div> | |
<div> | |
<button onClick={() => close(false)}>Cancel</button> | |
<button onClick={() => close(true)}>Ok</button> | |
</div> | |
</div> | |
) | |
}) | |
const clickMe = async () => { | |
const confirmed = await openMyModal() | |
if (confirmed) { | |
// do mutation | |
} | |
} | |
return ( | |
<button onClick={clickMe}>Open Modal</button> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment