fine-modal-react
v1.0.5
Published
Typed, promise-based modal utilities for React 19+.
Downloads
590
Maintainers
Readme
Fine Modal React
Typed, promise-based modals for React 19+ with two mounting strategies: a global host or colocated modal components.
Installation
npm install fine-modal-react
# or
pnpm add fine-modal-reactPeer deps: react and react-dom (19.x).
Quickstart (checklist)
- Install the package.
- Define your modal with
FineModal.define. - Aggregate modals in
modals.tsand augmentRegisterfor typedopen. - Choose mounting strategy:
- Global host: create
ModalHost = FineModal.createHost({ modals })near root. - Local: render the modal component where you need it.
- Global host: create
- Call
open(typed both ways):FineModal.open('ModalId', props?)uses registered ids/props/result frommodals.SomeModal.open(props?)uses the component’s props/result. Both resolve to theonConfirmvalue, ornullononCancel/close.
Define a modal (shared for both strategies)
ConfirmInviteModal.tsx
import { FineModal } from 'fine-modal-react'
interface ConfirmInviteModalProps {
initialProps: { email: string }
onConfirm: (value: 'sent') => void
onCancel: () => void
}
const ConfirmInvite = ({
initialProps,
onConfirm,
onCancel,
}: ConfirmInviteModalProps) => (
<section>
<p>Send an invite to {initialProps.email}?</p>
<div>
<button type="button" onClick={() => onConfirm('sent')}>Send</button>
<button type="button" onClick={onCancel}>Cancel</button>
</div>
</section>
)
export const ConfirmInviteModal = FineModal.define({
id: 'ConfirmInviteModal',
component: ConfirmInvite,
})modals.ts
import { ConfirmInviteModal } from './ConfirmInviteModal'
// Collect all modals in one place for the host and typed open()
export const modals = [ConfirmInviteModal] as const
// Module augmentation kept here for convenience; required for typed open()
declare module 'fine-modal-react' {
interface Register {
readonly modals?: typeof modals
}
}Option A: Global host (central place for all modals)
Use a single ModalHost near the app root. Open modals anywhere via their string id.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { FineModal } from 'fine-modal-react'
import App from './app'
import { modals } from './modals'
const ModalHost = FineModal.createHost({ modals })
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<ModalHost />
</StrictMode>
)// anywhere in the tree
import { FineModal } from 'fine-modal-react'
export function App() {
const handleInvite = async () => {
const result = await FineModal.open('ConfirmInviteModal', {
email: '[email protected]',
})
if (result === 'sent') {
console.log('Invite sent')
} else {
console.log('Invite cancelled')
}
}
return <button onClick={handleInvite}>Invite teammate</button>
}Type safety (global & local)
// Global host: ids/props/result inferred from registered modals
const result = await FineModal.open('ConfirmInviteModal', { email: '[email protected]' })
// ^? result is "sent" | null
// TS error: missing required prop "email"
FineModal.open('ConfirmInviteModal')
// TS error: unknown modal id
FineModal.open('UnknownModal')// Local modal: props/result inferred from component definition
const result = await ConfirmInviteModal.open({ email: '[email protected]' })
// ^? result is "sent" | null
// TS error: email must be a string
ConfirmInviteModal.open({ email: 42 })Option B: Local modal component (colocated scope)
Render the modal component where you need it; open it via its static API. This avoids a global host if you only need the modal in one subtree.
import { ConfirmInviteModal } from './ConfirmInviteModal'
export function App() {
const handleInvite = async () => {
const result = await ConfirmInviteModal.open({ email: '[email protected]' })
if (result === 'sent') {
console.log('Invite sent')
}
}
return (
<>
<button onClick={handleInvite}>Invite teammate</button>
<ConfirmInviteModal />
</>
)
}Modal authoring notes
onConfirm(value)resolves the promise returned byopenwithvalue.onCancel()resolves the promise withnulland closes the modal.- If your modal needs initial props, add an
initialPropsfield to the component props;openwill require/accept that shape. If not needed, omitinitialPropsandopen()will take no args. FineModal.openautomatically closes an existing modal with the same id before opening a new one.- Keep the module augmentation file (
modals.tsin the example) included intsconfigso TypeScript picks up theRegisterinterface extension.
