use-imperative-portal
v1.1.2
Published
A minimal, imperative portal library for React & React Native.
Maintainers
Readme
use-imperative-portal
A minimal, imperative portal library for React & React Native.
Control modals, toasts, and overlays from anywhere—inside or outside components—using standard Promises.
Key Features
- Universal: Works on React Native & Web (Endpoint-based).
- Hook-free: Call
openPortal()from utility functions (e.g., API interceptors). - Async-first: Simply
await portal.promisefor user interaction. - Type-safe: Strictly typed updates for dynamic content.
- Lightweight: Only 0.6kB (minzip). Zero boilerplate.
Install
npm i use-imperative-portalQuick start
- Place
<PortalEndpoint />at the root (or anywhere you want portals to render).
import { PortalEndpoint } from 'use-imperative-portal'
export default function App() {
return (
<Provider>
<MainContent />
{/* Portals will be rendered here. Contexts (Theme, I18n) propagate naturally. */}
<PortalEndpoint />
</Provider>
)
}- Open a portal from anywhere.
import { openPortal } from 'use-imperative-portal'
// Can be a regular function, not just a component or hook!
export async function handleDelete() {
// 1. Open
const portal = openPortal<boolean>(
<ConfirmDialog
title="Delete this item?"
onCancel={() => portal.close(false)}
onConfirm={() => portal.close(true)}
/>
)
// 2. Wait
const confirmed = await portal.promise
if (!confirmed) return
// 3. Process
await api.deleteItem()
}Examples
1. Await a result (Confirm / Prompt)
portal.close(value) resolves portal.promise with that value.
Confirm Dialog (Boolean)
import { openPortal } from 'use-imperative-portal'
export async function confirm(title: string) {
const portal = openPortal<boolean>(() => (
<Confirm title={title} onCancel={() => portal.close(false)} onOk={() => portal.close(true)} />
))
return await portal.promise
}
// Usage: if (await confirm('Are you sure?')) ...Prompt Input (Data)
export async function prompt(message: string) {
const portal = openPortal<string | null>(() => (
<Prompt message={message} onCancel={() => portal.close(null)} onSubmit={value => portal.close(value)} />
))
return await portal.promise
}
// Usage: const name = await prompt('Your name?')2. Type-safe updates (Progress bar)
Use a renderer function to enforce update arguments types.
import { openPortal } from 'use-imperative-portal'
// Define renderer: (number, string) => ReactNode
const renderer = (pct = 0, label = 'Ready') => (
<div>
<ProgressBar value={pct} />
<span>{label}</span>
</div>
)
export async function download() {
const portal = openPortal(renderer)
// portal.update(args) must match renderer's parameters
portal.update(0, 'Starting...')
for (let i = 0; i <= 100; i += 10) {
await sleep(100)
portal.update(i, 'Downloading...')
}
portal.close()
}3. Auto-close with AbortSignal
If the signal is aborted, the portal closes automatically.
import { openPortal } from 'use-imperative-portal'
export async function upload(file: File) {
const ac = new AbortController()
const portal = openPortal(<div>Uploading…</div>, { signal: ac.signal })
try {
await doUpload(file, { signal: ac.signal })
} finally {
// Safe even if already closed by ac.abort()
portal.close()
}
}API
openPortal(node, options?)
Returns a portal controller. The node argument determines how portal.update(...) works.
- Static Node:
openPortal(<div />)portal.update(newNode)replaces content.
- Renderer Function:
openPortal((a, b) => <div />)portal.update(a, b)calls the function with new args.- Use default parameters for the initial render (called with no args).
- Options:
{ signal?: AbortSignal }
Portal<Result, UpdaterArgs>
portal.isClosed:booleanportal.update(...args): Throws if closed.portal.close(value?): Resolves promise. Safe to call multiple times.portal.promise:Promise<Result>
PortalEndpoint
Renders all open portals where you place it in the tree. Contexts/styles propagate naturally.
createPortalContext()
Create separate “channels” (e.g. modals vs toasts).
import { createPortalContext } from 'use-imperative-portal'
const Toast = createPortalContext()
const Modal = createPortalContext()
export function Root() {
return (
<>
<Toast.Endpoint />
<App />
<Modal.Endpoint />
</>
)
}
export function openDialog() {
return Modal.openPortal(<Dialog />)
}Notes
- Lazy Evaluation: Renderer functions are evaluated lazily. This avoids TDZ issues and allows patterns like referencing
portalinside the renderer closure.
License
MIT © skt-t1-byungi
