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

@praha/toi

v1.1.0

Published

A tiny headless React utility for building imperative dialogs/toasts

Readme

@praha/toi

npm version npm download license Github

What "toi" Means

toi is the Japanese word 問い — a question (pronounced "toh-ee", close to the English toy). The kanji 問 is a mouth (口) set inside a gate (門): someone calling in at the gate and waiting for an answer. That's the whole library in one character — you ask a component something and await the answer:

const confirmed = await toi(Confirm);

The logo keeps the picture: the gate drawn in ink, and the mouth replaced by a vermillion seal — the answer already stamped and waiting inside. Read the full story in What "toi" Means.

👏 Getting Started

Installation

npm install @praha/toi

Usage

Render ToiHost once, anywhere in your component tree, to give toi a place to mount the components it renders.

import { ToiHost } from '@praha/toi';

const App = () => (
  <>
    <YourApp />
    <ToiHost />
  </>
);

Call toi with a component to mount it into the ToiHost and await the value passed to its resolve prop.

import { toi } from '@praha/toi';

import type { ToiProps } from '@praha/toi';
import type { FC } from 'react';

const Confirm: FC<ToiProps<boolean>> = ({ ref, resolve }) => (
  <dialog ref={ref} open>
    <button onClick={() => resolve(true)}>OK</button>
    <button onClick={() => resolve(false)}>Cancel</button>
  </dialog>
);

const confirmed = await toi(Confirm);

resolve can also be called with no argument for components that don't need to resolve with a value, such as toasts.

const Toast: FC<ToiProps> = ({ ref, resolve }) => (
  <div ref={ref} onAnimationEnd={() => resolve()}>
    Saved!
  </div>
);

await toi(Toast);

Components also receive a reject prop. Calling it rejects the promise instead, so await toi(Confirm) throws. It's meant for when the component can no longer answer — work it performs before resolving fails, or the user navigates away — not for ordinary dismissals like a "Cancel" button, which should resolve with a value.

const Confirm: FC<ToiProps<boolean>> = ({ ref, resolve, reject }) => (
  <dialog ref={ref} open>
    <button onClick={() => resolve(false)}>Cancel</button>
    <button
      onClick={async () => {
        try {
          await deleteItem();
          resolve(true);
        } catch (error) {
          reject(error);
        }
      }}
    >
      Delete
    </button>
  </dialog>
);

try {
  const deleted = await toi(Confirm);
} catch (error) {
  // deleteItem() failed
}

Called without a reason, reject rejects with a DOMException named AbortError, following the AbortSignal convention.

Note that navigating to another page doesn't reject the promise on its own. Components rendered by toi live in the ToiHost, which usually sits outside the routed part of your tree, so they stay mounted across page transitions. To close a component when the user navigates away, listen for the Navigation API's currententrychange event and call reject() from it.

import { useEffect } from 'react';

const Confirm: FC<ToiProps<boolean>> = ({ ref, resolve, reject }) => {
  useEffect(() => {
    const abandon = () => reject();
    navigation.addEventListener('currententrychange', abandon);
    return () => navigation.removeEventListener('currententrychange', abandon);
  }, [reject]);

  return (
    <dialog ref={ref} open>
      <button onClick={() => resolve(true)}>OK</button>
      <button onClick={() => resolve(false)}>Cancel</button>
    </dialog>
  );
};

Once resolve or reject is called, the component stays mounted until any running animations (excluding infinite ones) on the element attached to ref finish, so exit animations can play out before it's removed from the ToiHost and the promise settles.

Use toi.fn to bind a component to toi once and reuse the resulting function.

const confirm = toi.fn(Confirm);
const confirmed = await confirm();

Pass a second argument to toi for components that need additional props beyond ref, resolve, and reject.

type ConfirmProps = ToiProps<boolean> & { message: string };

const Confirm: FC<ConfirmProps> = ({ ref, resolve, message }) => (
  <dialog ref={ref} open>
    <p>{message}</p>
    <button onClick={() => resolve(true)}>OK</button>
    <button onClick={() => resolve(false)}>Cancel</button>
  </dialog>
);

const confirmed = await toi(Confirm, { message: 'Are you sure?' });

toi.fn's second argument works the same way, but as default props: they're used whenever the returned function is called without its own props argument, and can be overridden per call by passing props anyway.

const confirm = toi.fn(Confirm, { message: 'Are you sure?' });

const confirmed = await confirm(); // uses the default message: 'Are you sure?'
const confirmedAgain = await confirm({ message: 'Really?' }); // overrides it

🤝 Contributing

Contributions, issues and feature requests are welcome.

Feel free to check issues page if you want to contribute.

📝 License

Copyright © PrAha, Inc.

This project is MIT licensed.