npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@piotar/react-promise-bridge

v1.1.0

Published

A library that allows you to easily manage components that ultimately return a value as a `Promise` in `React`.

Readme

react-promise-bridge

GitHub package.json version npm (scoped) NPM

Render a React component and await the value it produces — as a Promise.

react-promise-bridge bridges the imperative world (call a function, get a result back) with the declarative world of React (render a component). You call open(<Dialog />), the component is mounted for you, and from inside it you call resolve(value) or reject(reason) to settle the promise and unmount the component.

It's a tiny, dependency-free primitive for anything that "asks the user something and returns an answer": confirm dialogs, modals, popups, toasts, notifications, color pickers, wizards — you own the UI, the library only owns the promise.

Why

Without it, asking the user a question means juggling state, callbacks, and conditional rendering:

// ❌ state + callbacks scattered across the component
const [confirmOpen, setConfirmOpen] = useState(false);
const onDelete = () => setConfirmOpen(true);
// ...somewhere in JSX:
{confirmOpen && <Confirm onYes={reallyDelete} onNo={() => setConfirmOpen(false)} />}

With react-promise-bridge the question reads top-to-bottom, like any other async call:

// ✅ imperative, linear, colocated
const onDelete = async () => {
    try {
        await open(<Confirm message="Delete this item?" />);
        reallyDelete();
    } catch {
        /* user cancelled */
    }
};

Features

  • Lightweight, zero dependencies (only a React peer dependency, >=16.6).
  • open() works inside and outside React — call it from event handlers, effects, or plain JS.
  • No prop drilling — the component talks back through React context, not props.
  • Bring your own component — no wrapper, no required props; existing components just work.
  • Multiple independent bridges and multiple / nested containers.
  • AbortSignal support — cancel pending entries from the outside.
  • Entry strategies — recreate or reject duplicates by id.
  • Deferred resolution for exit animations.

Installation

npm install @piotar/react-promise-bridge
# or: bun add / yarn add / pnpm add

Mental model

There are exactly three pieces:

| Piece | What it is | Who uses it | | --- | --- | --- | | Container | Where opened components are mounted | rendered once, anywhere in your tree (or a separate root) | | open(element) | Mounts element and returns a Promise | your app — event handlers, effects, plain JS | | usePromiseBridge() | Gives the mounted component { resolve, reject, signal } | the component passed to open() |

Container and open come as a pair from PromiseBridge.create(). Calling resolve/reject settles the promise returned by open and unmounts the component.

Quick start

1. Create a bridge (a Container + its open function):

// SystemPromiseBridge.ts
import { PromiseBridge } from '@piotar/react-promise-bridge';

// Names are yours to choose.
export const [Container, open] = PromiseBridge.create();

2. Mount the Container once — anywhere in the tree, or on a dedicated DOM root:

// main.tsx
import { Container } from './SystemPromiseBridge';

root.render(
    <App>
        {/* ...your app... */}
        <Container />
    </App>,
);

// or fully detached from the app tree:
// createRoot(document.getElementById('modals')!).render(<Container />);

3. Build a component that settles the promise via usePromiseBridge:

// Confirm.tsx
import { usePromiseBridge } from '@piotar/react-promise-bridge';

export function Confirm({ message }: { message: string }) {
    // <boolean> is the resolve type; reject reason defaults to `any`.
    const { resolve, reject } = usePromiseBridge<boolean>();

    return (
        <dialog open>
            <p>{message}</p>
            <button onClick={() => reject(new Error('Cancelled'))}>Cancel</button>
            <button onClick={() => resolve(true)}>Confirm</button>
        </dialog>
    );
}

4. open it and await the result — from inside or outside React:

import { open } from './SystemPromiseBridge';

async function confirmDelete() {
    try {
        await open<boolean>(<Confirm message="Delete this item?" />);
        console.log('confirmed');
    } catch (error) {
        console.warn('cancelled', error);
    }
}

⚠️ The Container must be mounted before you call open, otherwise open throws ContainerNotMountedException.

Open in StackBlitz

API

PromiseBridge.create(options?)

Returns [Container, open].

const [Container, open] = PromiseBridge.create({
    isMultiContainer: false, // allow more than one mounted Container for this bridge
    // container: CustomContainerComponent, // advanced: override the container renderer
});

open(element, options?) => Promise<T>

Mounts element in the Container and resolves/rejects when the component calls resolve/reject. Type the result with open<T>(...).

open<string>(<ColorPicker />, {
    signal,                          // AbortSignal — abort cancels the pending entry
    strategy: EntryStrategy.Recreate, // how to treat an entry with the same id
    id: 'colorPicker',               // required by Recreate / RejectIfExists strategies
});

Hooks

usePromiseBridge<T, R>()

The core hook. Read inside a component opened by open. Throws MissingContextException if used outside a bridged component.

const { resolve, reject, signal } = usePromiseBridge<ResolveType, RejectType>();

Example

useDeferredPromiseBridge<T, R>()

For exit animations. resolve/reject don't settle immediately — they move state to Pending so you can keep the component mounted and play a closing animation, then call trigger (e.g. in onAnimationEnd) to actually settle and unmount.

const { resolve, reject, signal, state, trigger, Provider } =
    useDeferredPromiseBridge<ResolveType, RejectType>();
// state is one of PromiseState.Initial | Pending | Settled

Example

useDisposePromiseBridge(signals?)

Returns an AbortController whose signal aborts when the calling (trigger) component unmounts — pass signal to open so any in-flight entry is rejected automatically when its opener disappears.

const { signal } = useDisposePromiseBridge();
await open(<ColorPicker />, { signal });

Example

useFactoryPromiseBridge(options?)

Creates a bridge scoped to the component instance (memoized), instead of a module-level singleton. Useful for nested/dynamic containers (e.g. a drawer that can open another drawer).

const [Container, open] = useFactoryPromiseBridge(options);

Example

Entry strategies — EntryStrategy

Passed via open(element, { strategy, id }) to control duplicate entries:

| Strategy | Behaviour | Needs id | | --- | --- | --- | | EntryStrategy.Normal (default) | Always opens a new entry. | no | | EntryStrategy.Recreate | Rejects an existing entry with the same id (with EntryRecreateException), then opens a fresh one. | yes | | EntryStrategy.RejectIfExists | If an entry with the same id already exists, the new open throws EntryExistsException. | yes |

Enums & types

  • PromiseState — Initial, Pending, Settled (used by useDeferredPromiseBridge).
  • AbortSignalStrategy — Any (abort if any source signal aborts) / Every (only when all do); used by ComposeAbortController when composing multiple signals.
  • ComposeAbortController — an AbortController that aborts based on a set of source signals.

Exceptions

All extend PromiseBridgeException, so you can catch broadly or narrow by class. Among them: ContainerNotMountedException, ContainerDestroyedException, ContainerLimitReachedException, EntryExistsException, EntryRecreateException, MissingEntryIdException, MissingContextException, TriggerComponentDestroyedException, and the abort-related EntryAbortedBySignalException / ExternalAbortSignalException.

Caveats & gotchas

A few behaviours are intentional but easy to trip over:

  • Always handle the promise — even fire-and-forget. A cancel is a rejection, and the library also rejects pending entries on its own: when the Container unmounts (ContainerDestroyedException), when a Recreate entry is replaced (EntryRecreateException), and when an AbortSignal fires. An open(...) whose result you never await/catch will surface as an unhandled promise rejection. For fire-and-forget, attach a no-op handler:

    open(<Toast message="Saved" />).catch(() => {});
  • Container must be mounted before open. Calling open with no mounted Container rejects with ContainerNotMountedException. By default only one Container may be mounted per bridge (a second throws ContainerLimitReachedException); opt into several with PromiseBridge.create({ isMultiContainer: true }).

  • Multi-container mirrors the same element. With isMultiContainer, an opened element is rendered in every mounted Container — i.e. the same component mounts more than once with independent local state. Settling from any copy resolves the single shared promise. Use it for intentional mirroring, not as a way to "pick" one container.

  • Remounting a Container rejects its pending entries. Unmounting a Container (conditional rendering, route changes, React Strict Mode's dev remount) dispatches a destroy that rejects any in-flight entries with ContainerDestroyedException. Keep the Container mounted for the lifetime of the flows it serves; in practice it mounts empty, so Strict Mode's double-invoke is harmless.

  • Pass stable options to useFactoryPromiseBridge. The bridge is memoized by the values of isMultiContainer and container, so an inline object is fine — but if you pass a signal array to useDisposePromiseBridge, give it a stable reference (e.g. useMemo), since a new array identity each render recomposes the controller.

Examples

| Repository example | Open in StackBlitz | | --- | --- | | #01 Basic | Open in StackBlitz | | #02 Animation | Open in StackBlitz | | #03 Animation with classes | Open in StackBlitz | | #04 Abort controller | Open in StackBlitz | | #05 Entry with strategy recreate | Open in StackBlitz | | #06 Entry with strategy reject if exists | Open in StackBlitz | | #07 Multicontainer | Open in StackBlitz | | #08 Destroy Bridge after destroy trigger component | Open in StackBlitz |

Integrations with UI libraries

| Repository example | Open in StackBlitz | | --- | --- | | #i01 Material UI (MUI) | Open in StackBlitz | | #i02 React Bootstrap | Open in StackBlitz | | #i03 Ant design (antd) | Open in StackBlitz |

AI agents

A SKILL.md (agentskills.io format) ships with the package so the library can be taught to AI agents and installed via a skill manager.

Claude Code plugin

The package doubles as a Claude Code plugin — a .claude-plugin/plugin.json manifest exposes the bundled SKILL.md as a react-promise-bridge skill. Load it straight from node_modules (no separate install):

claude --plugin-dir node_modules/@piotar/react-promise-bridge     # per-session

To make the skill available globally instead, symlink the installed package into your skills directory — it then auto-loads in every session:

ln -s "$(npm root)/@piotar/react-promise-bridge" ~/.claude/skills/react-promise-bridge

Either way Claude gains a react-promise-bridge skill that knows when and how to bridge a React component to an awaitable promise.

License

MIT © Piotr Tarasiuk