jlnstack

Introduction

Type-safe modal management for React

Modal

@jlnstack/modal version

A lightweight, type-safe modal management library for React with support for:

  • Type-safe modals with input/output generics
  • Builder API with fluent .input(), .output(), .template() methods
  • Lazy loading with lazy() for code-splitting modals
  • Position & size management for draggable/resizable modals
  • Z-index management with bringToFront, sendToBack, moveUp, moveDown

Installation

npm install @jlnstack/modal

Quick Start

1. Create a Modal

Use the modal builder to define type-safe modals:

import { modal } from "@jlnstack/modal";

// Simple modal with typed input
export const confirmModal = modal
  .input<{ title: string; message: string }>()
  .output<boolean>()
  .create((input, { resolve, close }) => (
    <div className="modal">
      <h2>{input.title}</h2>
      <p>{input.message}</p>
      <button onClick={() => resolve(true)}>Confirm</button>
      <button onClick={() => close()}>Cancel</button>
    </div>
  ));

2. Set up the Provider

import { createModalManager } from "@jlnstack/modal";
import { ModalProvider, ModalOutlet } from "@jlnstack/modal/react";

const manager = createModalManager();

function App() {
  return (
    <ModalProvider manager={manager}>
      <YourApp />
      <ModalOutlet />
    </ModalProvider>
  );
}

3. Open Modals

import { useModal } from "@jlnstack/modal/react";
import { confirmModal } from "./modals";

function DeleteButton() {
  const modal = useModal(confirmModal);

  const handleDelete = async () => {
    const confirmed = await modal.open({
      title: "Delete Item",
      message: "Are you sure you want to delete this item?",
    });

    if (confirmed) {
      // User clicked confirm
      deleteItem();
    }
  };

  return <button onClick={handleDelete}>Delete</button>;
}

The modal builder provides a fluent API for creating type-safe modals:

import { modal } from "@jlnstack/modal";

// Define input and output types
const myModal = modal
  .input<{ name: string }>()  // Type the input
  .output<string>()           // Type the resolved value
  .create((input, options) => <MyComponent {...input} {...options} />);

With Default Values

Provide default values to make input fields optional:

const greetModal = modal
  .input<{ name: string; greeting: string }>()
  .create(
    (input) => <p>{input.greeting}, {input.name}!</p>,
    {
      defaultValues: {
        modal: { greeting: "Hello" }  // greeting is now optional
      }
    }
  );

// Can open without greeting
greetModal.open({ name: "World" });

With Templates

Wrap modals in reusable templates:

const dialogModal = modal
  .output<boolean>()
  .template(({ modal, close, resolve }) => (
    <div className="dialog-wrapper">
      <div className="dialog-content">{modal}</div>
      <div className="dialog-footer">
        <button onClick={() => resolve(false)}>Cancel</button>
        <button onClick={() => resolve(true)}>OK</button>
      </div>
    </div>
  ))
  .create((input) => <p>{input.message}</p>);

Lazy Loading

Use lazy() to wrap modal imports for code-splitting. The modal is only loaded when first opened:

// modals/settings.ts - a normal modal definition
import { modal } from "@jlnstack/modal";

export const settingsModal = modal
  .input<{ userId: string }>()
  .create((input, { close }) => (
    <div className="modal">
      <h2>Settings for {input.userId}</h2>
      <button onClick={close}>Close</button>
    </div>
  ));
// modals/index.ts - wrap with lazy()
import { modal, lazy } from "@jlnstack/modal";

export const modals = {
  // Eager - loaded immediately with the registry
  confirm: modal.input<{ message: string }>().create(/* ... */),

  // Lazy - only loaded when first opened
  settings: lazy(() => import("./settings").then(m => m.settingsModal)),
};

You can provide a custom fallback while loading:

import { lazy } from "@jlnstack/modal";

const settingsModal = lazy(
  () => import("./settings").then(m => m.settingsModal),
  { fallback: <LoadingSpinner /> }
);

On this page