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 🙏

© 2025 – Pkg Stats / Ryan Hefner

imperative-portal

v1.5.0

Published

Render react nodes imperatively

Downloads

837

Readme

imperative-portal

A lightweight React library for rendering components imperatively with promise-based control. Perfect for modals, dialogs, notifications, and any UI that needs programmatic lifecycle management.

Table of Contents

Motivation

React encourages declarative UI patterns, but sometimes you want to render components imperatively - triggered by user actions, API calls, or other side effects that wouldn't fit nicely into your component tree without heavy boilerplate.

Common use cases include:

  • Modals and dialogs that need to be shown programmatically
  • Toast notifications and alerts
  • Confirmation dialogs
  • Input dialogs
  • Loading spinners or progress indicators
  • Any UI that appears/disappears based on imperative logic

Traditional approaches often involve:

  • Managing local or global state for UI visibility
  • Using complex portal-based setups

imperative-portal simplifies this by providing a clean API where you think of React nodes as promises.

Setup

npm install imperative-portal

Import the root component:

import { ImperativePortal } from "imperative-portal";

Add the <ImperativePortal /> element in your app, where you want your imperative nodes to be rendered. Typically near the root or even in a regular portal, but it's up to you.

Note: If you need some React contexts inside the imperative nodes, put <ImperativePortal /> as a descendant of their providers.

function App() {
  return (
    <div>
      <ImperativePortal />
    </div>
  );
}

Basic example

Open any React node programmatically by passing it to show:

import { show } from "imperative-portal"

const promise = show(
  <Toast>
    <h2>Hello World!</h2>
    <button onClick={() => promise.resolve()}>Close</button>
  </Toast>
);

setTimeout(() => promise.resolve(), 5000)

await promise; // Resolved when "Close" is clicked, or 5 seconds have passed

show returns a promise that tracks and controls the lifecycle of the node. Calling promise.resolve or promise.reject settles the promise and unmounts the node.

You can also pass a node factory:

show(promise => (
  <Toast>
    <button onClick={() => promise.resolve()}>Close</button>
  </Toast>
));

Confirm dialog example

You can get back data from the imperative node via the promise:

function confirm(message: string) {
  return show<boolean>(promise => (
    <Dialog open onOpenChange={open => !open && promise.resolve(false)}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Confirm</DialogTitle>
          <DialogDescription>{message}</DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button onClick={() => promise.resolve(true)}>Confirm</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  ));
}

if (await confirm("Delete this item?")) {
  console.log("Deleted!");
} else {
  console.log("Cancelled");
}

useImperativePromise hook

For components that need to control their lifecycle from within, you can use closures and props as shown above, or pass the promise directly as prop, but simpler would be to use the useImperativePromise hook:

import { show, useImperativePromise } from "imperative-portal";

function SelfManagedDialog() {
  const promise = useImperativePromise();
  return (
    <Dialog open onOpenChange={open => !open && promise.reject()}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Self-managed dialog</DialogTitle>
          <DialogDescription>
            This dialog controls its lifecycle from within without props
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button onClick={() => promise.resolve()}>Confirm</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

try {
  await show(<SelfManagedDialog />);
} catch {
  console.log("Cancelled");
}

Input dialog example

Here's an advanced example that captures user input from a text field:

function NamePromptDialog() {
  const promise = useImperativePromise<string>();
  const [name, setName] = useState("");
  return (
    <Dialog open onOpenChange={open => !open && promise.reject()}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Enter your name</DialogTitle>
          <DialogDescription>Please enter your name.</DialogDescription>
        </DialogHeader>
        <Input
          autoFocus
          value={name}
          onChange={e => setName(e.target.value)}
          placeholder="Your name..."
        />
        <DialogFooter>
          <DialogClose asChild>
            <Button variant="outline">Cancel</Button>
          </DialogClose>
          <Button onClick={() => promise.resolve(name)}>Submit</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

try {
  const name = await show<string>(<NamePromptDialog />);
  console.log(`Hello, ${name}!`);
} catch {
  console.log("Name prompt cancelled");
}

Node update

You can update the rendered node while it's still mounted:

const promise = show(<div>Loading...</div>);

// Later, update the content
promise.update(<div>Done!</div>);

// Close it
promise.resolve();

You can use the render function pattern for dynamic content:

const renderProgress = (value: number) => (
  <div>
    <div>Progress: {value}%</div>
    <progress value={value} max={100} />
  </div>
);

const promise = show(renderProgress(0));

// Later, update with new progress value
promise.update(renderProgress(50));

// Complete the progress
promise.update(renderProgress(100));

// Close it
promise.resolve();

Like show, update can also accept a node factory:

const promise = show(<div>Loading...</div>);

promise.update(promise => (
  <button onClick={() => promise.resolve()}>Done!</button>
));

Advanced features

Checking settlement status

const promise = show(<MyComponent />);

// Check if the node has been closed
if (promise.settled) {
  console.log("Portal is closed");
}

Wrap prop

The ImperativePortal component accepts an optional wrap prop that allows you to wrap active imperative nodes with custom JSX. Useful for basic customization.

import { AnimatePresence } from "motion/react";

function App() {
  return (
    <ImperativePortal
      wrap={nodes => <AnimatePresence>{nodes}</AnimatePresence>}
    />
  );
}

Advanced customization

For more advanced rendering scenarios that require access to individual node keys, promises or elements, use the useImperativePortal hook to create a custom imperative portal component:

import { useImperativePortal } from "imperative-portal";
import { AnimatePresence, motion } from "motion/react";

function CustomImperativePortal() {
  const nodes = useImperativePortal();
  return (
    <AnimatePresence>
      {nodes.length > 0 && (
        <motion.div
          key="backdrop"
          className="fixed inset-0 bg-black/50"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onClick={() => nodes.forEach(n => n.promise.reject())}
        />
      )}
      {nodes.map((n, i) => (
        <motion.div
          key={n.key}
          initial={{ opacity: 0, scale: 0.8, y: 20 + i * 10 }}
          animate={{ opacity: 1, scale: 1, y: i * 10 }}
          exit={{ opacity: 0, scale: 0.8, y: 20 + i * 10 }}
        >
          {n.node}
        </motion.div>
      ))}
    </AnimatePresence>
  );
}

function App() {
  return (
    <div>
      <CustomImperativePortal />
    </div>
  );
}

Note: When wrapping individual nodes like in this example, make sure to include the key={n.key} prop on your wrapper element for proper React reconciliation.

Multiple portal systems

Use createImperativePortal to create isolated portal systems that can be mounted at different locations:

import { createImperativePortal } from "imperative-portal";

const [ModalPortal, showModal, useModalPortal] = createImperativePortal();
const [ToastPortal, showToast, useToastPortal] = createImperativePortal();

function App() {
  return (
    <div>
      <ModalPortal />
      <ToastPortal />
    </div>
  );
}

showModal(<MyModal />);
showToast(<MyToast />);

API reference

Component ImperativePortal

A React component that renders all active imperative nodes. Typically placed near the root of your app. Takes an optional wrap prop for basic customization.

Function show<T>(node: ReactNodeOrFactory<T>): ImperativePromise<T>

Renders a React node imperatively and returns a promise that tracks and controls the lifecycle of the node. You can pass either a React node directly or a function that receives the imperative promise and returns a React node.

Hook useImperativePortal(): ImperativeNode<any>[]

A React hook that returns the array of all active imperative nodes in the portal system. Each node is an object containing key, node (the React node), and promise properties. Useful for advanced customization.

Hook useImperativePromise<T>(): ImperativePromise<T>

A React hook that provides access to the imperative portal promise from within a rendered imperative node. Must be used within components that are rendered via the show function. Enables self-contained imperative components that can control their own lifecycle.

Function createImperativePortal()

Creates a new imperative portal system with its own store. Returns an [ImperativePortal, show, useImperativePortal] tuple.

Types

ImperativePromise<T>

Extends Promise<T> with additional properties:

  • settled: boolean - Whether the promise has been resolved or rejected.
  • resolve(value: T): void - Resolves the promise, unmounts the node.
  • reject(reason?: any): void - Rejects the promise, unmounts the node.
  • update(node: ReactNodeOrFactory<T>): void - Updates the node. You can pass either a React node directly or a function that receives the imperative promise and returns a React node.

ImperativeNode<T>

  • key: string - A unique identifier for the node, used for React reconciliation.
  • node: ReactNode - The React node.
  • promise: ImperativePromise<T> - The promise that controls the node's lifecycle.

ReactNodeOrFactory<T>

A React node or a function that receives an imperative promise and returns a React node:

type ReactNodeOrFactory<T> =
  | ReactNode
  | ((promise: ImperativePromise<T>) => ReactNode);