Skip to content

Instantly share code, notes, and snippets.

@nicksheffield
Last active January 23, 2023 05:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicksheffield/cdd6de59592bda6c41cda286549d905d to your computer and use it in GitHub Desktop.
Save nicksheffield/cdd6de59592bda6c41cda286549d905d to your computer and use it in GitHub Desktop.
react awaitable imperative modal paradigm
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