Skip to content

Repository files navigation

awaitable-component

Show React components and await a result – turn any component (modals, dialogs, prompts, ...) into an async function

npm version License: MIT

Installation

npm install awaitable-component
# or
yarn add awaitable-component
# or
pnpm add awaitable-component

Requires: React >= 18.0.0

Quick Start

Use the default AwaitableComponent instance (or create your own instances).

1. Mount the Root

import { AwaitableComponent } from 'awaitable-component';

function App() {
  return (
    <>
      <AwaitableComponent.Root />
      <YourApp />
    </>
  );
}

2. Create an Awaitable Component

import { AwaitableComponentProps } from 'awaitable-component';

// Example with MUI dialog API
function ConfirmDialog({ title, message, onExitComplete, onSubmit, onDismiss, isVisible }: AwaitableComponentProps<boolean> & {
    title: string;
    message: string;
}) {
  return (
    <Dialog open={isVisible} onTransitionExited={onExitComplete}>
      <h2>{title}</h2>
      <p>{message}</p>
      <button onClick={() => onDismiss()}>Cancel</button>
      <button onClick={() => onSubmit(true)}>Confirm</button>
    </Dialog>
  );
}

3. Use It Anywhere

Use show() to simply call and await the result, or use showControlled() for control over the component lifecycle.

async function handleDelete() {
  const confirmed = await AwaitableComponent.show(ConfirmDialog, {
    title: 'Delete Item',
    message: 'Are you sure?'
  });

  if (confirmed) {
    await deleteItem();
  }
}

Usage Examples

Basic Usage

Use show() to simply await the result of a component.

const result = await AwaitableComponent.show(MyComponent, {
  prop1: 'value'
}, {
  id: 'unique-id',      // Optional: reuse/update existing component
  scope: 'custom',      // Optional: target specific root
  exitTimeout: 300      // Optional: override default timeout
});

Controlled Mode

Use showControlled() to have control over the component lifecycle.

async function uploadFile(file: File) {
  const controlled = AwaitableComponent.showControlled(ProgressDialog, {
    title: 'Uploading...',
    progress: 0
  });

  for (let i = 0; i <= 100; i += 10) {
    await delay(100);
    controlled.update({ progress: i });  // Update props while visible (partial props supported)
  }

  controlled.submit({ success: true });  // Programmatically submit
  return controlled.promise;
}

Partial prop updates:

The update() method accepts partial props, allowing you to update only specific properties:

const controlled = AwaitableComponent.showControlled(StatusDialog, {
  title: 'Processing...',
  message: 'Starting upload',
  progress: 0
});

// Update only progress
controlled.update({ progress: 50 });

// Update only message
controlled.update({ message: 'Finalizing...' });

// Update multiple props
controlled.update({ progress: 100, message: 'Complete!' });

Bound Components

Create bound versions of components for simple reuse:

// Bind a confirmation dialog for easy reuse
const ConfirmDialog = AwaitableComponent.create(Dialog);

// Use it anywhere without passing the Dialog component
async function handleDelete() {
  const confirmed = await ConfirmDialog.show({
    title: 'Delete Item',
    message: 'Are you sure?'
  });

  if (confirmed) {
    await deleteItem();
  }
}

async function handleLogout() {
  const confirmed = await ConfirmDialog.show({
    title: 'Logout',
    message: 'Are you sure you want to logout?'
  });

  if (confirmed) {
    await logout();
  }
}

Bound component root placement:

By default, bound components render in their parent's AwaitableComponent.Root. However, they provide their own root which can be rendered anywhere in your app for specific DOM placement:

// Default: renders in parent root
function App() {
  return (
    <>
      <AwaitableComponent.Root />  {/* ConfirmDialog renders here by default */}
      <YourApp />
    </>
  );
}

// Optional: render in specific location
function SomePlaceInYourApp() {
  return (
    <>
      <ConfirmDialog.Root />  {/* ConfirmDialog renders here instead */}
      {/* ... */}
    </>
  );
}

Multiple AwaitableComponent Instances

Create separate AwaitableComponent instances.

import { AwaitableComponentClass } from 'awaitable-component';

const UserAppAwaitableComponent = new AwaitableComponentClass(500);  // Optional exit timeout
const AdminAppAwaitableComponent = new AwaitableComponentClass();

function UserApp() {
  return (
    <>
      <UserAppAwaitableComponent.Root />
      <UserContent />
    </>
  );
}

function AdminApp() {
  return (
    <>
      <AdminAppAwaitableComponent.Root />
      <AdminContent />
    </>
  );
}

Exit Animations

Built-in lifecycle hooks for smooth transitions. Use onExitComplete to hide the component after the exit animation completes, and/or define an exitTimeout to remove the component after a specified timeout. Set the timeout to 0 to remove the component immediately after dismissal or submission.

function AnimatedDialog({
  isVisible,
  onExitComplete,
  onSubmit,
  onDismiss
}: AwaitableComponentProps<string>) {
  return (
    <Dialog
      open={isVisible}
      onTransitionEnd={onExitComplete}  // ✓ Preferred: precise control
      className={isVisible ? 'fade-in' : 'fade-out'}
    >
      <button onClick={() => onSubmit('confirmed')}>OK</button>
    </Dialog>
  );
}

Animation lifecycle:

  1. Component renders with isVisible: true
  2. User action triggers onSubmit or onDismiss
  3. Component re-renders with isVisible: false (trigger exit animation)
  4. Component unmounts when whichever comes first:
    • onExitComplete() is called (precise control, best result)
    • exitTimeout expires

Exit timeout configuration:

const AwaitableDialog = new AwaitableComponentClass(300);  // Fallback for everything

await AwaitableDialog.show(AnimatedDialog, props, {
  exitTimeout: 300  // Overrides all defaults
});

const BoundDialog = AwaitableDialog.create(AnimatedDialog, {
  scope: 'my-scope',
  exitTimeout: 250  // Default for all BoundDialog.show() calls
});

API Reference

Default Instance: AwaitableComponent

The library exports a default instance for immediate use.

It has the exitTimeout disabled and requires onExitComplete to be called or a exitTimeout to be passed on each show call to hide components.

import { AwaitableComponent } from 'awaitable-component';

Creating Multiple Instances: new AwaitableComponentClass(exitTimeout?)

Create separate instances for architectural separation:

import { AwaitableComponentClass } from 'awaitable-component';

const UserAppDialogs = new AwaitableComponentClass(250);
const AdminAppDialogs = new AwaitableComponentClass(300);

Parameters:

  • exitTimeout (optional) - Default exit timeout for this instance (default: null (disabled))

Each instance has its own Root, show(), showControlled() and create() methods.


AwaitableComponent.Root

Root component that renders active components. Mount once in your app:

<AwaitableComponent.Root />

AwaitableComponent.show(component, props, options?)

Show a component and await its result:

const result = await AwaitableComponent.show(MyComponent, {
  prop1: 'value'
}, {
  id: 'unique-id',      // Optional: reuse/update existing component
  scope: 'custom',      // Optional: target specific bound component
  exitTimeout: 300      // Optional: override exit timeout (highest priority)
});

Returns: Promise<T> that resolves on submitting and rejects on dismissing

Options:

  • exitTimeout (optional) - Overrides bound component and global defaults
  • scope (optional) - Target specific bound component
  • id (optional) - Unique component identifier

AwaitableComponent.showControlled(component, props, options?)

Show with programmatic control:

const controlled = AwaitableComponent.showControlled(MyComponent, props);

controlled.update({ newProp: 'value' });   // Update props
controlled.submit(result);                 // Resolve promise
controlled.dismiss(error);                 // Reject promise
await controlled.promise;                  // Access underlying promise

Returns: ControlledAwaitableComponent

  • update(props) - Update props (accepts partial props)
  • submit(result) - Resolve promise
  • dismiss(error) - Reject promise
  • promise - Access underlying promise

Options:

  • exitTimeout (optional) - Overrides bound component and global defaults
  • scope (optional) - Target specific bound component
  • id (optional) - Unique component identifier

AwaitableComponent.create(component, options?)

Create a bound component for easy reuse:

const ConfirmDialog = AwaitableComponent.create(Dialog, {
  id: 'confirm-dialog',  // Optional: unique bound component identifier (auto-generated if omitted)
  exitTimeout: 300       // Optional: default for all ConfirmDialog.show() calls
});

// Bound root is optional - if not rendered, uses parent AwaitableComponent.Root
<ConfirmDialog.Root />               // Optional: render in specific location
await ConfirmDialog.show(props);     // Show component
ConfirmDialog.showControlled(props); // Show with control

Options:

  • id (optional) - Unique bound component identifier (auto-generated if not provided)
  • exitTimeout (optional) - Default exit timeout for all show() calls on this bound component (overrides instance default, can be overridden per-call)

This timeout is used as a fallback when onExitComplete is not called. It has the lowest priority and is overridden by:

  1. Per-call exitTimeout in show() options
  2. Bound component exitTimeout in create() options

Component Props Interface

Your components should extend AwaitableComponentProps<T>:

interface AwaitableComponentProps<T = void> {
  onSubmit: (value: T) => void;       // Resolve the promise
  onDismiss: (reason?: any) => void;  // Reject the promise
  isVisible: boolean;                 // False during exit animation
  onExitComplete?: () => void;        // Preferred: call when animation finishes
}

Animation props:

  • isVisible - Toggles to false when component should exit (use this to trigger your exit animation)
  • onExitComplete - Call this when your exit animation completes for precise timing (preferred over relying on exitTimeout)

License

MIT

About

Show React components and await a result

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages