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

use-modal-ref

v1.0.1

Published

react hooks for modal usage

Downloads

421

Readme

use-modal-ref

npm version npm downloads license Test coverage typescript react

🚀 Powerful React hooks for elegant modal/drawer management

English | 中文

💡 Why this library?

Introducing async/await to JavaScript was a major step forward: nested callback pyramids could be written in a linear, almost synchronous style, avoiding callback hell.

With React, Vue, and other MVVM frameworks, modals are usually controlled by visible / open props. A business flow like “open dialog → wait for user → continue based on result” is one logical sequence, but it gets split into:

  • Parent state for visible and for payload passed into the modal (props / context)
  • Follow-up logic inside onOk / onCancel / onClose, where the return value (form data, confirm choice) is delivered—not back to the function that opened the dialog
  • Multiple chained dialogs pushing logic into nested callbacks or ad-hoc state machines

Modal interaction became callback-driven again at the UI layer (even when you do pass data in and get a result out of the modal UI itself).

use-modal-ref brings modal-driven workflows back to async/await: callers await modalRef.current.modal(...), components finish with endModal / cancelModal, so a full business path stays linear instead of being chopped into scattered handlers.

Flow comparison

Traditional approach: state + callbacks split the flow

flowchart TD
  A[User clicks Submit] --> B["setPayload(order) + setVisible(true)"]
  B --> C[Modal renders with props/data]
  C --> D{User action}
  D -->|OK| E["onOk(result) — return value arrives here only"]
  D -->|Cancel| F["onCancel"]
  E --> G["setVisible(false)"]
  F --> G
  G --> H["Continue in onOk callback<br/>(save API / open next modal…)"]
  H --> I["Next modal → another handler;<br/>caller cannot await a return value"]

With use-modal-ref: write the flow in order

flowchart TD
  A[User clicks Submit] --> B["result = await modalRef.modal(data)"]
  B --> C[Modal opens, Promise pending]
  C --> D{User action}
  D -->|OK| E["modal.endModal(result)"]
  D -->|Cancel| F["modal.cancelModal()"]
  E --> G[Promise resolves]
  F --> H[Promise rejects]
  G --> I["Code after await runs next"]
  H --> I
  I --> J["await another modal in the same function<br/>linear end-to-end flow"]

Code comparison

function OrderPage() {
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [editOpen, setEditOpen] = useState(false);
  const [confirmPayload, setConfirmPayload] = useState(null);
  const [editPayload, setEditPayload] = useState(null);
  const [draft, setDraft] = useState({ items: [] });

  const handleSubmit = () => {
    // Pass data in: stash payload, then open (not return values to this function)
    setConfirmPayload({ order: draft });
    setConfirmOpen(true);
  };

  const handleConfirmOk = async (confirmed) => {
    // Return value out: modal calls onOk with user input (or reads form ref inside)
    setConfirmOpen(false);
    setConfirmPayload(null);

    const saved = await saveOrder(confirmed);
    setEditPayload(saved);
    setEditOpen(true);
  };

  const handleEditOk = (edited) => {
    setEditOpen(false);
    setEditPayload(null);
    message.success('Done');
  };

  return (
    <>
      <Button onClick={handleSubmit}>Submit order</Button>
      <ConfirmModal
        open={confirmOpen}
        data={confirmPayload}
        onOk={handleConfirmOk}
        onCancel={() => setConfirmOpen(false)}
      />
      <EditModal
        open={editOpen}
        data={editPayload}
        onOk={handleEditOk}
        onCancel={() => setEditOpen(false)}
      />
    </>
  );
}

Traditional modals can pass parameters (props / state) and surface a result via onOk. What breaks the linear story is that handleSubmit cannot await that result—the next step must live in handleConfirmOk, then handleEditOk.

function OrderPage() {
  const confirmRef = useRef(null);
  const editRef = useRef(null);
  const [draft, setDraft] = useState(null);

  const handleSubmit = async () => {
    const ok = await confirmRef.current.modal({ order: draft });
    const saved = await saveOrder(ok);
    const edited = await editRef.current.modal(saved);
    message.success('Done');
  };

  return (
    <>
      <Button onClick={handleSubmit}>Submit order</Button>
      <ConfirmModal ref={confirmRef} />
      <EditModal ref={editRef} />
    </>
  );
}

On cancel, the modal calls cancelModal() and the Promise rejects—the async handler stops at that await; no undefined check needed.

When it helps

  • Process-heavy UX: wizards, confirm-then-edit, double confirm before delete
  • You want one async function for open → wait → branch → continue, not many visible flags and handlers
  • Imperative APIs (e.g. showRefModal) that still need an awaitable result

Note: Modal UI stays declarative (visible / {...modal.props}). This library changes how callers structure business flow—from callback-driven back to await-driven.

✨ Features

  • 🎯 Simple & Intuitive - Easy-to-use hooks for modal/drawer management
  • 🔄 Async/Await Support - Return values from modals with Promise-based API
  • 🎨 Framework Agnostic - Works with any UI library (Antd, Material-UI, etc.)
  • 🔧 Extensible - Support custom modal types (Popover, Tooltip, etc.)
  • 📦 Lightweight - Minimal runtime deps (@babel/runtime only; React as peer), no UI library dependencies, TypeScript support
  • 🎭 Multiple Usage Patterns - Ref-based, function-based, and component-based approaches

📦 Installation

npm install use-modal-ref
yarn add use-modal-ref
pnpm add use-modal-ref

Local example (Playground)

Runnable demo: examples/ (Vite + Ant Design 5, sidebar + five README examples).

Dependencies and scripts live in the root package.json:

yarn install
yarn demo

Demo deps and scripts live in the root package.json. Vite aliases to src/ for hot reload. Run yarn build and use "use-modal-ref": "file:.." to validate the npm package layout.

React version compatibility

| Item | Notes | |------|-------| | peerDependencies | react, react-dom >= 16.8.0 (minimum for Hooks) | | Verified | React 16 (unit tests), 18 (examples demo) | | React 19 | Uses only stable Hooks APIs (useState, useEffect, useMemo, useCallback, useImperativeHandle); no legacy context or string refs. No known React 19 incompatibilities—please open an Issue if you hit one | | Strict Mode | Supported; demo runs inside <React.StrictMode> | | UI libraries | Works with Ant Design, MUI, etc.; for Ant Design 5+, call mergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } }) at app entry |

Browser compatibility

The package ships three JS builds:

| Entry / condition | Build | Format | Syntax target | Minimum (reference) | |-------------------|-------|--------|---------------|---------------------| | import (default) | esm/ | ESM | ES6 | Chrome ≥ 49 | | require (main) | cjs/ | CJS | ES6 | Chrome ≥ 49 | | use-modal-ref/es | es/ | ESM | ES2020 | Chrome ≥ 87 |

// Default — Chrome ≥ 49 ESM (Vite / webpack / Rolldown)
import useModalRef from 'use-modal-ref';

// Modern-only ESM entry
import useModalRef from 'use-modal-ref/es';

// Node / older tooling
const useModalRef = require('use-modal-ref');

CI validates builds with es-check (esm/+cjs/ → ES6, es/ → ES2020). Your bundler targets and polyfills still apply on top of these baselines.

Note: 1.0.0 incorrectly published CJS under both es/ and esm/ while exports.import pointed at CJS; that broke default-import interop in Rolldown/Vite (default is not a function). Fixed in 1.0.1 with real ESM defaults that remain Chrome ≥ 49.

Feature-specific Web APIs:

| Feature | Required APIs | |---------|---------------| | Hooks (useModalRef, etc.) | React client runtime only — no DOM | | createRefComponent / showRefModal | DOM + react-dom | | createUrlListener / getUrlListener | window + History API by default; mergeNativeAdapter({ urlListener }) for other runtimes |

Runtime: Web SSR / RSC / React Native

Hooks (useModalRef / useDrawerRef / useCommonRef) depend only on the React client runtime (useState, useEffect, etc.) and do not touch window / document. They work in browsers, React Native, and any client environment that supports Hooks.

createRefComponent / showRefModal default to DOM + react-dom (mount adapter injectable). URL listening defaults to History API (noop without DOM). On React Native, use mergeNativeAdapter to declare capabilities (null = explicitly unsupported, undefined = keep Web default).

Server Components (Next.js App Router) cannot use Hooks regardless of platform. The main entry is marked 'use client' for framework boundaries.

| API | Web browser | React Native | SSR / RSC | |-----|-------------|--------------|-----------| | useModalRef / useDrawerRef / useCommonRef | ✅ | ✅ (bind RN Modal, etc.) | ✅ in Client Components | | createRefComponent / showRefModal | ✅ | ✅ (mergeNativeAdapter / createInTreeMountAdapter) | ❌ without DOM + default adapter | | createUrlListener / getUrlListener | ✅ | ✅ (mergeNativeAdapter({ urlListener })) | default noop; urlListener: null = permanent noop | | mergeModalType | ✅ | ✅ | ✅ in Server Components | | isBrowser() | usually true | usually false | false on SSR |

isBrowser() means DOM is available (window + document), not “Hooks are allowed”. On React Native it is false, but Hooks still work.

React Native example (declarative modal + await):

// React Native only — for Web use antd / MUI (see Quick Start above)
import React from 'react';
import { Modal, View, Text, Button } from 'react-native';
import useModalRef from 'use-modal-ref';

const AppModal = React.forwardRef((_, ref) => {
  const { modal, data } = useModalRef(ref, { title: 'Hello' });

  return (
    <Modal
      visible={modal.props.visible}
      onRequestClose={modal.props.onCancel}
    >
      <View>
        <Text>{data.title}</Text>
        <Button title="OK" onPress={() => modal.endModal('ok')} />
      </View>
    </Modal>
  );
});

React Native / non-DOM: mergeNativeAdapter

import {
  mergeNativeAdapter,
  createInTreeMountAdapter,
  showRefModal,
} from 'use-modal-ref';

// Declare native capabilities at startup (null = explicitly unsupported)
mergeNativeAdapter({
  id: 'react-native',
  refComponentMount: myMountAdapter,
  urlListener: myUrlAdapter,
});

const { adapter: inTreeAdapter, Host: RefMountHost } = createInTreeMountAdapter();

function App() {
  useEffect(() => {
    mergeNativeAdapter({ refComponentMount: inTreeAdapter });
    return () => mergeNativeAdapter(null);
  }, []);

  return (
    <>
      <RefMountHost />
      <Main />
    </>
  );
}

Legacy mergeRefComponentMount / mergeUrlListenerAdapter still work (they delegate to mergeNativeAdapter).

URL listener (closeWhenUrlChange):

import { mergeNativeAdapter, showRefModal } from 'use-modal-ref';

mergeNativeAdapter({
  urlListener: {
    id: 'react-navigation',
    createUrlListener: () => ({
      addListener(listener) {
        return navigation.addListener('state', () => listener(getCurrentRouteUrl()));
      },
    }),
  },
});

void showRefModal(MyModal, data, { closeWhenUrlChange: true, urlListenerAdapter: myAdapter });

Next.js App Router example:

// app/layout.tsx — Server Component, global config only
import { mergeModalType } from 'use-modal-ref/config';

mergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } });
'use client';

import useModalRef from 'use-modal-ref';

const UserModal = React.forwardRef((_, ref) => {
  const { modal, data } = useModalRef(ref, { title: 'User' });
  return <Modal {...modal.props} title={data.title} />;
});
'use client';

import { showRefModal, isBrowser } from 'use-modal-ref';

function Page() {
  const open = () => {
    if (!isBrowser()) return;
    void showRefModal(MyModal, { title: 'Hello' });
  };
  return <button type="button" onClick={open}>Open</button>;
}

React 16+: 'use client' is ignored as a plain string on React 16/17 (CRA, Vite, etc.). Environment checks use only typeof window / typeof document — no React 18+ APIs.

🚀 Quick Start

Basic Modal Usage

import React, { useState, useRef } from 'react';
import { Modal, Button, Input } from 'antd';
import useModalRef from 'use-modal-ref';

// Modal Component
const TestModal = React.forwardRef((props, ref) => {
  const [inputValue, setInputValue] = useState('');

  const { modal, data } = useModalRef(ref, {
    title: 'Default Title',
    label: 'Default Label'
  });

  const handleOK = () => modal.endModal(inputValue);
  const handleCancel = () => modal.cancelModal();

  return (
    <Modal
      {...modal.props}
      title={data.title}
      footer={[
        <Button key="cancel" onClick={handleCancel}>Cancel</Button>,
        <Button key="ok" type="primary" onClick={handleOK}>OK</Button>
      ]}
    >
      <div>{data.label}</div>
      <Input 
        value={inputValue} 
        onChange={e => setInputValue(e.target.value)} 
        placeholder="Enter value..."
      />
    </Modal>
  );
});

// Usage Component
function App() {
  const modalRef = useRef(null);

  const showModal = async () => {
    const result = await modalRef.current.modal({
      title: 'Custom Title',
      label: 'Please enter a value:'
    });
    
    if (result !== undefined) {
      alert(`You entered: ${result}`);
    }
  };

  return (
    <div>
      <Button type="primary" onClick={showModal}>
        Show Modal
      </Button>
      <TestModal ref={modalRef} />
    </div>
  );
}

📚 Usage Examples

1. Modal with Antd

import React, { useState, useRef } from 'react';
import { Modal, Button, Input, Form } from 'antd';
import useModalRef from 'use-modal-ref';

const UserModal = React.forwardRef((props, ref) => {
  const [form] = Form.useForm();
  const [loading, setLoading] = useState(false);

  const { modal, data } = useModalRef(ref, {
    title: 'Add User',
    user: null
  }, {
    // You can perform initialization operations in this event, and the function's return value will be used as the new data returned from the useModalRef hook's data property
    beforeModal({ user }) {
      form.setFieldsValue(user || {
        name: '',
        email: '',
      });
    },
    // You can perform cleanup operations in this event, and the function's return value will be used as the new data returned from the useModalRef hook's data property.
    afterModalClose() {
     setLoading(false);
      form.setFieldsValue({
        name: '',
        email: '',
      });
    }
  });

  const handleSubmit = async () => {
    try {
      setLoading(true);
      const values = await form.validateFields();
      modal.endModal(values);
    } catch (error) {
      console.error('Validation failed:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleCancel = () => modal.cancelModal();

  return (
    <Modal
      {...modal.props}
      title={data.title}
      confirmLoading={loading}
      onOk={handleSubmit}
      onCancel={handleCancel}
    >
      <Form form={form} layout="vertical">
        <Form.Item
          name="name"
          label="Name"
          rules={[{ required: true, message: 'Please enter name' }]}
        >
          <Input placeholder="Enter name" />
        </Form.Item>
        <Form.Item
          name="email"
          label="Email"
          rules={[
            { required: true, message: 'Please enter email' },
            { type: 'email', message: 'Please enter valid email' }
          ]}
        >
          <Input placeholder="Enter email" />
        </Form.Item>
      </Form>
    </Modal>
  );
});

// Usage
function UserManagement() {
  const userModalRef = useRef(null);

  const addUser = async () => {
    const userData = await userModalRef.current.modal({
      title: 'Add New User'
    });
    
    if (userData) {
      console.log('New user:', userData);
      // Handle user creation
    }
  };

  return (
    <div>
      <Button type="primary" onClick={addUser}>
        Add User
      </Button>
      <UserModal ref={userModalRef} />
    </div>
  );
}

2. Drawer with Antd

By default, the drawer type maps to visible + onClose, which matches Ant Design 4 Drawer.
Ant Design 5+ uses open for visibility—call mergeModalType once at app entry (see example below).

import React, { useState, useRef } from 'react';
import { Drawer, Button, Input, Space } from 'antd';
import { useDrawerRef, mergeModalType } from 'use-modal-ref';

// Ant Design 5+: map drawer visibility to `open` (run once at app entry)
mergeModalType({
  drawer: {
    visible: 'open',
    onClose: 'onClose',
  },
});

const SettingsDrawer = React.forwardRef((props, ref) => {
  const [settings, setSettings] = useState({});

  const { modal, data } = useDrawerRef(ref, {
    title: 'Settings',
    initialSettings: {}
  }, {
    beforeModal: async (data) => {
      setSettings(data.initialSettings);
      return data;
    }
  });

  const handleSave = () => {
    modal.endModal(settings);
  };

  const handleCancel = () => {
    modal.cancelModal();
  };

  // modal.props maps to { open, onClose } after mergeModalType above
  return (
    <Drawer
      {...modal.props}
      title={data.title}
      width={400}
      footer={
        <Space>
          <Button onClick={handleCancel}>Cancel</Button>
          <Button type="primary" onClick={handleSave}>Save</Button>
        </Space>
      }
    >
      <Space direction="vertical" style={{ width: '100%' }}>
        <Input
          placeholder="Setting 1"
          value={settings.setting1 || ''}
          onChange={e => setSettings(prev => ({ ...prev, setting1: e.target.value }))}
        />
        <Input
          placeholder="Setting 2"
          value={settings.setting2 || ''}
          onChange={e => setSettings(prev => ({ ...prev, setting2: e.target.value }))}
        />
      </Space>
    </Drawer>
  );
});

// Usage
function SettingsPage() {
  const settingsRef = useRef(null);

  const openSettings = async () => {
    const newSettings = await settingsRef.current.modal({
      title: 'Edit Settings',
      initialSettings: { setting1: 'value1', setting2: 'value2' }
    });
    
    if (newSettings) {
      console.log('Updated settings:', newSettings);
    }
  };

  return (
    <div>
      <Button onClick={openSettings}>Open Settings</Button>
      <SettingsDrawer ref={settingsRef} />
    </div>
  );
}

3. Custom overlay with explicit modal.props binding

When your overlay is not Ant Design Modal / Drawer (or does not accept a single spread object), bind visibility and close explicitly instead of {...modal.props}:

  • modal.props.visible — whether the overlay is open
  • modal.props.onCancel — wired to modal.cancelModal() (the awaited modal() Promise rejects on cancel)

Runnable demo: yarn demoModal props binding (examples/src/demos/ModalPropsBindingDemo.tsx, component examples/src/components/PropsBindingModal.tsx).

import React, { useRef, useState } from 'react';
import useModalRef, { ModalRef } from 'use-modal-ref';

type FormData = { title: string; hint: string };

/** Custom overlay — only needs visible + onCancel from modal.props */
function YourOverlay({
  visible,
  onCancel,
  title,
  onOk,
  children,
}: {
  visible: boolean;
  onCancel: () => void;
  title: string;
  onOk?: () => void;
  children: React.ReactNode;
}) {
  if (!visible) return null;
  return (
    <div role="dialog" aria-modal="true">
      <h3>{title}</h3>
      {children}
      <button type="button" onClick={onCancel}>Cancel</button>
      {onOk && <button type="button" onClick={onOk}>OK</button>}
    </div>
  );
}

const PropsBindingModal = React.forwardRef<
  ModalRef<'modal', FormData, string>
>((_, ref) => {
  const [value, setValue] = useState('');

  const { modal, data } = useModalRef(ref, {
    title: 'Default title',
    hint: 'Default hint',
  }, {
    beforeModal: (payload) => {
      setValue('');
      return payload;
    },
  });

  const handleOk = () => {
    const trimmed = value.trim();
    if (!trimmed) return;
    modal.endModal(trimmed);
  };

  return (
    <YourOverlay
      visible={modal.props.visible}
      onCancel={modal.props.onCancel}
      title={data.title}
      onOk={handleOk}
    >
      <p>{data.hint}</p>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && handleOk()}
      />
    </YourOverlay>
  );
});

function Page() {
  const modalRef = useRef<ModalRef<'modal', FormData, string>>(null);

  const openModal = async () => {
    try {
      const result = await modalRef.current!.modal({
        title: 'Edit',
        hint: 'Enter a value',
      });
      console.log('Result:', result);
    } catch {
      console.log('Cancelled');
    }
  };

  return (
    <>
      <button type="button" onClick={openModal}>Open</button>
      <PropsBindingModal ref={modalRef} />
    </>
  );
}

Notes:

  • For built-in type modal, modal.props is { visible, onCancel } by default — Ant Design users often use <Modal {...modal.props} />; custom overlays use the two fields above.
  • ref.current.modal({ title: undefined }) does not overwrite defaultData.title (explicit undefined is skipped when merging open payload).
  • Confirm with modal.endModal(value); cancel via overlay close / modal.props.onCancelcancelModal → Promise rejection.

4. JavaScript modal with JSDoc types

In .js projects, declare modal data and ref types with JSDoc and import ModalRef from the package — no .ts files required. IDE checks apply when checkJs is enabled in jsconfig.json / tsconfig.json.

Runnable demo: yarn demo7. JavaScript + JSDoc (#/usage/js-radio-modal); sources examples/src/components/RadioModal.js, examples/src/demos/JsRadioModalDemo.tsx.

// RadioModal.js
import React, { useState } from 'react';
import { Modal, Radio } from 'antd';
import useModalRef from 'use-modal-ref';

/**
 * @typedef {{
 *   title?: string,
 *   width?: number,
 *   okText?: string,
 *   message: Array<{
 *     value: string,
 *     text: string,
 *   }>,
 * }} RadioModalData
 */

/**
 * ModalRef<P, T, U> — P: overlay type, T: data, U: result from endModal
 * @typedef {import('use-modal-ref').ModalRef<'modal', RadioModalData, string>} RadioModalRef
 */

const RadioModal = React.forwardRef(
  /**
   * @param {Record<string, never>} props
   * @param {React.ForwardedRef<RadioModalRef>} ref
   */
  (props, ref) => {
    /** @type {[string | null, React.Dispatch<React.SetStateAction<string | null>>]} */
    const [selectValue, setSelectValue] = useState(null);

    const {
      modal,
      data: { title, width, okText, message },
    } = useModalRef(ref, /** @type {RadioModalData} */ ({
      title: 'Notice',
      width: 560,
      okText: 'OK',
      message: [],
    }), {
      beforeModal({ message: options }) {
        setSelectValue(options[0]?.value ?? null);
      },
    });

    const handleOk = () => {
      if (selectValue != null) {
        modal.endModal(selectValue);
      }
    };

    return (
      <Modal
        {...modal.props}
        title={title}
        width={width}
        okText={okText}
        onOk={handleOk}
        destroyOnClose
      >
        <Radio.Group
          value={selectValue}
          onChange={(e) => setSelectValue(e.target.value)}
        >
          {message.map((item) => (
            <Radio key={item.value} value={item.value}>{item.text}</Radio>
          ))}
        </Radio.Group>
      </Modal>
    );
  },
);

RadioModal.displayName = 'RadioModal';
export default RadioModal;
// Page.jsx — caller; import types from RadioModal.js via JSDoc import()
import React, { useRef } from 'react';
import { Button } from 'antd';
import RadioModal from './RadioModal';

function Page() {
  /** @type {React.MutableRefObject<import('./RadioModal').RadioModalRef | null>} */
  const radioModalRef = useRef(null);

  const openRadio = async () => {
    try {
      const picked = await radioModalRef.current.modal(
        /** @type {import('./RadioModal').RadioModalData} */ ({
          title: 'Choose one',
          message: [
            { value: 'a', text: 'Option A' },
            { value: 'b', text: 'Option B' },
          ],
        }),
      );
      console.log('Picked:', picked);
    } catch {
      console.log('Cancelled');
    }
  };

  return (
    <>
      <Button type="primary" onClick={openRadio}>Open</Button>
      <RadioModal ref={radioModalRef} />
    </>
  );
}

JSDoc checklist:

| Goal | Pattern | |------|---------| | Modal data shape | @typedef { ... } RadioModalData | | Ref type (data + result U) | @typedef {import('use-modal-ref').ModalRef<'modal', RadioModalData, string>} RadioModalRef | | forwardRef ref param | @param {React.ForwardedRef<RadioModalRef>} ref | | defaultData literal | /** @type {RadioModalData} */ ({ ... }) | | useRef in caller | @type {React.MutableRefObject<import('./RadioModal').RadioModalRef \| null>} | | Reference data type in caller | import('./RadioModal').RadioModalData |

Pattern adapted from production RadioModal.js: define @typedef once in the modal component file; callers reference import('./RadioModal').RadioModalRef / RadioModalData without redeclaring.

5. Custom Popover Component

// usePopoverRef.js
import { useCommonRef, mergeModalType } from 'use-modal-ref';

// Register popover modal type
mergeModalType({
  popover: {
    visible: 'visible',
    onClose: 'onClose',
  }
});

const usePopoverRef = (ref, defaultData, options, deps = []) => 
  useCommonRef('popover', ref, defaultData, options, deps);

export default usePopoverRef;
// ColorPickerPopover.jsx
import React, { useState } from 'react';
import { Popover, Button, Space } from 'antd';
import usePopoverRef from './usePopoverRef';

const ColorPickerPopover = React.forwardRef((props, ref) => {
  const [selectedColor, setSelectedColor] = useState('#1890ff');

  const { modal, data } = usePopoverRef(ref, {
    title: 'Choose Color',
    colors: ['#1890ff', '#52c41a', '#faad14', '#f5222d']
  });

  const handleColorSelect = (color) => {
    setSelectedColor(color);
    modal.endModal(color);
  };

  return (
    <Popover
      {...modal.props}
      title={data.title}
      content={
        <Space direction="vertical">
          {data.colors.map(color => (
            <Button
              key={color}
              style={{ 
                backgroundColor: color, 
                borderColor: color,
                width: 40,
                height: 40
              }}
              onClick={() => handleColorSelect(color)}
            />
          ))}
        </Space>
      }
    >
      {props.children}
    </Popover>
  );
});

// Usage
function ColorPicker() {
  const colorRef = useRef(null);

  const pickColor = async () => {
    const color = await colorRef.current.modal({
      title: 'Select Color',
      colors: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1']
    });
    
    if (color) {
      console.log('Selected color:', color);
    }
  };

  return (
    <div>
      <ColorPickerPopover ref={colorRef}>
        <Button onClick={pickColor}>Pick Color</Button>
      </ColorPickerPopover>
    </div>
  );
}

6. Function-based Modal

import React from 'react';
import { Button } from 'antd';
import TestModal from './TestModal';
import { showRefModal } from 'use-modal-ref';

function App() {
  const showModal = async () => {
    const result = await showRefModal(TestModal, {
      title: 'Dynamic Modal',
      label: 'This modal was created dynamically'
    });
    
    if (result) {
      alert(`Modal result: ${result}`);
    }
  };

  return (
    <div>
      <Button type="primary" onClick={showModal}>
        Show Dynamic Modal
      </Button>
    </div>
  );
}

7. Component-based Modal

import React from 'react';
import { Button } from 'antd';
import TestModal from './TestModal';
import { createRefComponent } from 'use-modal-ref';

function App() {
  const showModal = async () => {
    const [ref, destroy] = await createRefComponent(TestModal);
    
    try {
      const result = await ref.modal({
        title: 'Temporary Modal',
        label: 'This modal will be destroyed after use'
      });
      
      if (result) {
        alert(`Result: ${result}`);
      }
    } finally {
      destroy(); // Clean up the component
    }
  };

  return (
    <div>
      <Button type="primary" onClick={showModal}>
        Show Temporary Modal
      </Button>
    </div>
  );
}

🔧 API Reference

useModalRef

function useModalRef<T = any, U = any, C extends Record<string, any> = {}>(
  ref: React.Ref<any>,
  defaultData?: Partial<T> | (() => Partial<T>),
  options?: ModalRefOption<'modal', T, U, C>,
  deps?: React.DependencyList
): {
  modal: ModalRef<'modal', T, U, C>;
  data: T;
  setData: (newData: T | ((data: T) => T)) => void;
}

Generic parameters:

  • T extends Record<string, any> - Shape of modal data (defaults to any)
  • U = any - Promise resolve type from modal()
  • C extends Record<string, any> = {} - Type of custom properties in options.custom, merged onto ModalRef

Parameters:

  • ref: React.Ref<any> - React ref object for controlling modal show/hide. Must be passed to the modal component via React.forwardRef
  • defaultData?: Partial<T> | (() => Partial<T>) - Default data, can be an object or a function returning an object. When using a function, it supports lazy initialization and only executes when needed
  • options?: ModalRefOption<'modal', T, U, C> - Hook config and lifecycle callbacks; see ModalRefOption
  • deps?: React.DependencyList - When deps change, re-syncs lifecycle callbacks and custom internally (does not change modal identity; lifecycle hooks are not on the public modal API—configure them via options)

Returns:

  • modal: ModalRef<'modal', T, U, C> - Modal control object with:
    • visible: boolean - Read-only visibility flag
    • data: T - Read-only current modal data
    • props: { visible: boolean, onCancel: () => void } - Spread onto Ant Design Modal (visible / onCancel)
    • options: ModalRefOption<'modal', T, U, C> - Read-only merged hook options
    • modalOptions: ModalModalOptions - Read-only runtime options from the latest modal() call
    • modalPromise: Promise<U> | null - Read-only current Promise, null when closed
    • modal(newData?: Partial<T>, options?: ModalModalOptions): Promise<U> - Open modal and return Promise
    • endModal(result?: U, onDone?: () => void): Promise<void> - Confirm close and resolve Promise
    • cancelModal(ex?: any, onDone?: () => void): Promise<void> - Cancel; rejects unless alwaysResolve is set
  • data: T - Current modal data (same as modal.data)
  • setData: (newData: T | ((data: T) => T)) => void - Update modal data (object or updater)

ModalRefOption

The options parameter of useModalRef, useDrawerRef, and useCommonRef shares this type (generic P matches the overlay type).

type ModalRefOption<
  P extends ExtendedModalType,
  T extends Record<string, any>,
  U,
  C extends Record<string, any> = {},
  R = unknown
> = {
  useImperativeHandle?: boolean;
  alwaysResolve?: boolean;
  visibleKey?: string;
  custom?: C;
  beforeModal?: (
    newData: Partial<T>,
    pause: (result: any, isError?: boolean) => void,
    options: Record<string, any>
  ) => void | Partial<T> | Promise<void | Partial<T>>;
  afterModal?: (newData: any, options?: ModalModalOptions) => void | Promise<void>;
  init?: (newData: T, options: Record<string, any>) => void | Promise<void>;
  beforeCloseModal?: (
    next: (ok: any) => void,
    action: ModalAction,
    modal: ModalRef<P, T, U>
  ) => void | Promise<void>;
  afterCloseModal?: (
    newData: T,
    action: ModalAction,
    modal: ModalRef<P, T, U>
  ) => void | Promise<void>;
  [key: string]: any;
}

Properties:

Lifecycle hooks (beforeModal, afterModal, etc.) are configured via options only—they are not part of the public modal / ref.current type.

  • useImperativeHandle?: boolean - Whether to expose modal on ref via useImperativeHandle. Defaults to true (only false disables it). Treat as constant—do not change at runtime after the first render

  • alwaysResolve?: boolean - When true, cancelModal resolves the Promise instead of rejecting

  • visibleKey?: string - Overrides the default visibility prop name from modalTypeMap, producing modal.props.[visibleKey]. For Ant Design 5 Drawer, use 'open'; see mergeModalType for global mapping

  • custom?: C - Custom properties merged onto modal, accessible via modal.xxx or ref.current.xxx. Must not use built-in modal property names (e.g. modal, visible, endModal, beforeModal); conflicting keys are ignored with a dev warning

    custom example:

    import React, { useState, useRef } from 'react';
    import { Modal } from 'antd';
    import useModalRef, { ModalRef } from 'use-modal-ref';
    
    const TestModal = React.forwardRef((props, ref) => {
      const [formData, setFormData] = useState({});
      const resetForm = () => setFormData({});
      const validate = () => Object.keys(formData).length > 0;
      const { modal, data } = useModalRef<{ title: string }, any>(
        ref,
        { title: 'Default Title' },
        { custom: { resetForm, validate } }
      );
      return (
        <Modal {...modal.props} title={data.title}>
          <button onClick={() => modal.resetForm()}>Reset</button>
        </Modal>
      );
    });
    
    function App() {
      const modalRef = useRef<ModalRef<'modal'>>(null);
      return (
        <>
          <button onClick={() => modalRef.current?.modal({ title: 'Test' })}>Open</button>
          <TestModal ref={modalRef} />
        </>
      );
    }
  • beforeModal?: (...) - Pre-open hook. May return Partial<T> to merge into data; call pause(result, isError?) to abort (isError: true throws)

    • newData - Data passed to this modal() call
    • pause - Abort/pause callback
    • options - Runtime options for this modal() call
  • afterModal?: (newData, options?) => void | Promise<void> - Post-open hook (recommended). Hook-level options.afterModal and per-call modal(data, { afterModal }) each fire once and do not replace each other

  • init?: (newData, options) => void | Promise<void> - Legacy post-open callback (older API); kept for compatibility—prefer afterModal for new code. If both are set, init runs first, then afterModal

  • beforeCloseModal?: (next, action, modal) => void | Promise<void> - Pre-close hook; call next(false) to prevent closing

    • action - 'end' (confirm) or 'cancel'
  • afterCloseModal?: (newData, action, modal) => void | Promise<void> - Post-close hook; return value becomes new data

mergeModalType

Globally override prop field names on modal.props (visibility and close callback keys).

function mergeModalType(map: Partial<MergeModalTypeInput>): MergeableModalTypeMap

type MergeModalTypeInput = Partial<{
  [K in keyof MergeableModalTypeMap]: ModalTypeItem;
}>;

Parameters:

  • map: Partial<MergeModalTypeInput> - Partial map to merge. Covers built-in types and types registered via module augmentation. For Ant Design 5 Drawer using open:
import { mergeModalType } from 'use-modal-ref';

mergeModalType({
  drawer: { visible: 'open', onClose: 'onClose' },
});
// All drawer instances then get modal.props { open, onClose }

For a single hook instance only, set options.visibleKey instead of changing the global map.

useDrawerRef

function useDrawerRef<
  T extends Record<string, any> = any,
  U = any,
  C extends Record<string, any> = {}
>(
  ref: React.Ref<any>,
  defaultData?: Partial<T> | (() => Partial<T>),
  options?: ModalRefOption<'drawer', T, U, C>,
  deps?: React.DependencyList
): {
  modal: ModalRef<'drawer', T, U, C>;
  data: T;
  setData: (newData: T | ((data: T) => T)) => void;
}

Generic parameters: Same T, U, C as useModalRef.

Parameters:

  • ref: React.Ref<any> - React ref for drawer show/hide
  • defaultData?: Partial<T> | (() => Partial<T>) - Default data (object or lazy factory)
  • options?: ModalRefOption<'drawer', T, U, C> - Hook config; see ModalRefOption
  • deps?: React.DependencyList - Re-sync options onto modal when deps change

Returns:

  • modal: ModalRef<'drawer', T, U, C> - Drawer control object; default modal.props is { visible, onClose } (map to { open, onClose } via mergeModalType or visibleKey)
  • data: T - Current drawer data
  • setData: (newData: T | ((data: T) => T)) => void - Update drawer data

With Ant Design Drawer: Built-in drawer exposes modal.props.visible by default. Ant Design 5+ Drawer expects open—call mergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } }) at app entry, or set visibleKey: 'open' in hook options for a single instance.

useCommonRef

function useCommonRef<
  P extends ModalType = ModalType,
  T extends Record<string, any> = any,
  U = any,
  C extends Record<string, any> = {}
>(
  modalType: P,
  ref: React.Ref<any>,
  defaultData?: Partial<T> | (() => Partial<T>),
  options?: ModalRefOption<P, T, U, C>,
  deps?: React.DependencyList
): {
  modal: ModalRef<P, T, U, C>;
  data: T;
  setData: (newData: T | ((data: T) => T)) => void;
}

Generic parameters:

  • P extends ModalType - Overlay type ('modal', 'drawer', 'popover', or extended via mergeModalType)
  • T, U, C - Same as useModalRef

Parameters:

  • modalType: P - Overlay type identifier
  • ref: React.Ref<any> - React ref for show/hide control
  • defaultData?: Partial<T> | (() => Partial<T>) - Default data (object or lazy factory)
  • options?: ModalRefOption<P, T, U, C> - Hook config; see ModalRefOption
  • deps?: React.DependencyList - Re-sync options onto modal when deps change

Returns:

  • modal: ModalRef<P, T, U, C> - Overlay control object
  • data: T - Current data
  • setData: (newData: T | ((data: T) => T)) => void - Update data

showRefModal

function showRefModal<
  M extends ModalRef<any, any>,
  D extends M extends ModalRef<any, infer D> ? D : Record<string, any>,
  U extends M extends ModalRef<any, any, infer U> ? U : any
>(
  RefModal: React.ForwardRefExoticComponent<React.RefAttributes<M>>,
  modalData?: D,
  options?: {
    /** Component props */
    props?: Record<string, any>;
    /** Method name to call modal, defaults to 'modal' */
    modalMethod?: string;
    /** Method name to cancel modal, defaults to 'cancelModal' */
    cancelMethod?: string;
    /** Modal options */
    modalOptions?: ModalModalOptions;
    /** Whether to automatically close modal when URL changes */
    closeWhenUrlChange?: boolean;
    /** Container selector */
    selector?: string;
    /** Container ID */
    id?: string;
    /** Container element or function returning container */
    container?: HTMLElement | null | (() => HTMLElement);
    /** Container class name */
    className?: string;
    /** Callback after ref is created */
    onRef?: (ref: any, destroy: () => void) => void;
    /** Callback before container is appended, return true to prevent auto append */
    onAppendContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before container is removed, return true to prevent auto remove */
    onRemoveContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before component is destroyed, return true to prevent auto destroy */
    onDestroyComponent?: (container: HTMLElement) => void | boolean;
    /** Destroy delay in milliseconds, defaults to 50 */
    destroyDelay?: number;
  }
): Promise<U>

Parameters:

  • RefModal: React.ForwardRefExoticComponent<React.RefAttributes<M>> - Modal component to display, must be wrapped with React.forwardRef
  • modalData?: D - Data to pass to the modal, will be passed as the first argument to the modal method
  • options?: object - Configuration options object with the following properties:
    • props?: Record<string, any> - Props to pass to the component
    • modalMethod?: string - Method name to call modal, defaults to 'modal'
    • cancelMethod?: string - Method name to cancel modal, defaults to 'cancelModal'
    • modalOptions?: ModalModalOptions - Modal options, will be passed as the second argument to the modal method
    • closeWhenUrlChange?: boolean - Whether to automatically close modal when URL changes, defaults to false
    • selector?: string - Container selector for finding existing container element
    • id?: string - Container ID for finding or creating container element
    • container?: HTMLElement | null | (() => HTMLElement) - Container element or function returning container
    • className?: string - Container class name, only used when creating new container
    • onRef?: (ref: any, destroy: () => void) => void - Callback function after ref is created
    • onAppendContainer?: (container: HTMLElement) => void | boolean - Callback before container is appended to DOM, return true to prevent auto append
    • onRemoveContainer?: (container: HTMLElement) => void | boolean - Callback before container is removed from DOM, return true to prevent auto remove
    • onDestroyComponent?: (container: HTMLElement) => void | boolean - Callback before component is destroyed, return true to prevent auto destroy
    • destroyDelay?: number - Destroy delay in milliseconds, defaults to 50

Returns:

  • Promise<U> - Returns modal result Promise, will be rejected if user cancels

createRefComponent

function createRefComponent<
  T extends React.ForwardRefExoticComponent<React.RefAttributes<any>>,
  P extends T extends React.ForwardRefExoticComponent<React.RefAttributes<infer P>> ? P : never,
  R extends T extends React.ForwardRefExoticComponent<React.RefAttributes<P & infer R>> ? R : never
>(
  RefComponent: T,
  props?: P | null,
  options?: {
    /** Container selector */
    selector?: string;
    /** Container ID */
    id?: string;
    /** Container element or function returning container */
    container?: HTMLElement | null | (() => HTMLElement);
    /** Container class name */
    className?: string;
    /** Callback after ref is created */
    onRef?: (ref: any, destroy: () => void) => void;
    /** Callback before container is appended, return true to prevent auto append */
    onAppendContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before container is removed, return true to prevent auto remove */
    onRemoveContainer?: (container: HTMLElement) => void | boolean;
    /** Callback before component is destroyed, return true to prevent auto destroy */
    onDestroyComponent?: (container: HTMLElement) => void | boolean;
    /** Destroy delay in milliseconds, defaults to 50 */
    destroyDelay?: number;
  }
): Promise<[ref: R, destroy: () => void]>

Parameters:

  • RefComponent: T - Component to create, must be wrapped with React.forwardRef
  • props?: P | null - Props to pass to the component
  • options?: object - Configuration options object with the following properties:
    • selector?: string - Container selector for finding existing container element. If found, will use that container; otherwise creates a new one
    • id?: string - Container ID for finding or creating container element. If found, will use that container; otherwise creates a new one and sets this ID
    • container?: HTMLElement | null | (() => HTMLElement) - Container element or function returning container. If provided, will use this container directly
    • className?: string - Container class name, only used when creating new container
    • onRef?: (ref: any, destroy: () => void) => void - Callback function after ref is created, receives ref object and destroy function
    • onAppendContainer?: (container: HTMLElement) => void | boolean - Callback function before container is appended to DOM, return true to prevent auto append to document.body
    • onRemoveContainer?: (container: HTMLElement) => void | boolean - Callback function before container is removed from DOM, return true to prevent auto remove
    • onDestroyComponent?: (container: HTMLElement) => void | boolean - Callback function before component is destroyed, return true to prevent auto call to ReactDOM.unmountComponentAtNode
    • destroyDelay?: number - Destroy delay in milliseconds, defaults to 50. Used to delay destroy operation to avoid incomplete animations

Returns:

  • Promise<[ref: R, destroy: () => void]> - Returns a Promise that resolves to a tuple:
    • ref: R - Component's ref object containing all methods and properties exposed by the component
    • destroy: () => void - Destroy function that will unmount the component and remove the container (if container was auto-created)

ModalRef

ModalRef is the type definition for the modal control object, containing the following properties and methods:

type ModalRef<
  P extends ModalType = 'modal',
  T extends Record<string, any> = Record<string, any>,
  U = any,
  C extends Record<string, any> = {}
> = {
  /** Read-only property: whether modal is visible */
  readonly visible: boolean;
  /** Read-only property: current modal data */
  readonly data: Partial<Omit<T, 'onCancel'|'onOK'>> & { [key: string]: any };
  /** Read-only property: props to pass to Modal/Drawer component */
  readonly props: ModalPropsTypeMap[P];
  /** Read-only property: current configuration options */
  readonly options: ModalRefOption<P, T, U, C>;
  /** Read-only property: current modal options */
  readonly modalOptions: ModalModalOptions;
  /** Read-only property: current modal Promise */
  readonly modalPromise: null | Promise<any> | PromiseLike<any>;

  /** Opens modal and returns Promise */
  modal(newData: T, options?: ModalModalOptions): Promise<U>;

  /** Ends modal and returns result */
  endModal: (result?: U, onDone?: () => void) => Promise<void>;
  
  /** Cancels modal */
  cancelModal: (ex?: any, onDone?: () => void) => Promise<void>;

  [key: string]: any;
} & {
  [key in keyof C]: C[key];
}

Property Descriptions:

  • visible: boolean - Read-only property indicating whether the modal is currently visible
  • data: T - Read-only property containing the current modal data object
  • props: ModalPropsTypeMap[P] - Read-only property returning appropriate props based on modal type:
    • For 'modal' type: { visible: boolean, onCancel: () => void }
    • For 'drawer' type: { visible: boolean, onClose: () => void } by default; use generic VK (e.g. ModalRef<'drawer', T, U, C, 'open'>) after mergeModalType or visibleKey: 'open'
    • For 'popover' type: { visible: boolean, onClose: () => void }
  • options: ModalRefOption<P, T, U, C> - Read-only property containing current configuration options
  • modalOptions: ModalModalOptions - Read-only property containing current modal options
  • modalPromise: Promise<U> | null - Read-only property containing current modal Promise, null if modal is not open

Method Descriptions:

  • modal(newData: T, options?: ModalModalOptions): Promise<U> - Method to open the modal
    • newData: T - New data to pass to the modal, will be merged with defaultData
    • options?: ModalModalOptions - Modal options with the following properties:
      • modalDataEvent?: boolean - Legacy data-payload callbacks (optional). When true, you may pass onOK / onCancel on the newData object of modal(newData, { modalDataEvent: true }):

        • On open, those keys are removed from data (so they are not rendered or serialized with business fields).
        • On endModal(value), onOK(value) runs first; if it returns a value other than undefined, that becomes the await modal() resolve value (before beforeEndModal).
        • On cancelModal(reason), onCancel(reason) runs similarly and may rewrite the reject reason (before beforeCancelModal).
        • Field names are onOK (capital OK) and onCancel — not Ant Design’s onOk.
        • New code: prefer await modal() + beforeEndModal / beforeCancelModal in modal() options; use modalDataEvent only when migrating old APIs that attached OK/Cancel handlers to the data payload.
        const result = await modalRef.current!.modal(
          {
            title: 'Confirm',
            onOK: async (value) => `processed-${value}`,
            onCancel: async () => 'user aborted',
          },
          { modalDataEvent: true },
        );
        // endModal('x') → result is 'processed-x'
        // cancelModal()   → Promise rejects with 'user aborted'
      • afterModal?: (newData: any, options?: ModalModalOptions) => void | Promise<void> - Hook after modal opens

      • beforeCloseModal?: (next: (ok: any) => void, action: ModalAction) => void | Promise<void> - Hook before close

      • beforeEndModal?: ModalBeforeEndHandler<U> - Pre-end hook; may rewrite the resolved value. Receives current U from endModal, returns U | undefined | void

      • beforeCancelModal?: (reason?: any) => Promise<any> - Hook before cancel, can modify cancel reason

      • afterCloseModal?: (newData: any, action: ModalAction, modal: ModalRef<any, any, any>) => void | Promise<void> - Hook after close

      • forceVisible?: boolean - Force show, even if another modal is already open

      • alwaysResolve?: boolean - Whether to always resolve the Promise

    • Returns Promise<U>, resolves with result, rejects when cancelled
  • endModal(result?: U, onDone?: () => void): Promise<void> - Ends the modal
    • result?: U - Result value to return
    • onDone?: () => void - Callback function after completion
  • cancelModal(ex?: any, onDone?: () => void): Promise<void> - Cancels the modal
    • ex?: any - Cancel reason, defaults to 'cancel'
    • onDone?: () => void - Callback function after completion

getUrlListener

Gets the URL listener instance for listening to browser URL changes.

function getUrlListener(): UrlListener

interface UrlListener {
  addListener: (
    listener: (url: string) => void,
    options?: { once?: boolean }
  ) => () => void;
}

Returns:

  • UrlListener - URL listener object with the following method:
    • addListener(listener: (url: string) => void, options?: { once?: boolean }): () => void - Adds a URL change listener
      • listener: (url: string) => void - Listener function called when URL changes, receives new URL as parameter
      • options?: { once?: boolean } - Options object, when once is true, listener executes only once
      • Returns a function to remove the listener

Usage Example:

import { getUrlListener } from 'use-modal-ref';

const urlListener = getUrlListener();

// Add listener
const removeListener = urlListener.addListener((url) => {
  console.log('URL changed to:', url);
}, { once: true });

// Remove listener
removeListener();

📘 TypeScript Support

This library is written in TypeScript and provides full type definitions out of the box.

Exported types

Key types importable from use-modal-ref:

| Type | Description | |------|-------------| | ModalRef<P, T, U, C, VK, R> | Six-parameter ref: overlay type, data, result, custom, visibility key, cancel reason | | ModalRefOption<P, T, U, C, R> | Hook options | | ModalModalOptions<U, R> | Runtime modal() options | | UseCommonRefReturn<P, T, U, C, VK, R> | Hook return value | | ModalCustomMap<C> / ResolveCustomMap<C, O> | Custom keys excluding builtins / inferred from options | | ResolveVisibleKey<VK, O> / ExtractVisibleKey<O> | Infer props field from options.visibleKey literal | | ModalBeforeEndHandler<U> / ModalBeforeCancelHandler<R> | Typed before-end / before-cancel handlers | | ExtendedModalType / ModalTypeRegistry | Built-in + augmentable overlay types | | MergeModalTypeInput / MergeableModalTypeMap | mergeModalType input and map types |

Generics U / R / VK

// U: endModal / modal() Promise result
const ref = useRef<ModalRef<'modal', FormData, SubmitResult>>(null);
const { modal } = useModalRef<FormData, SubmitResult>(ref, defaultData);
await modal.endModal({ ok: true });

// R: beforeCancelModal return type
type CancelPayload = { code: number; message: string };
const cancelRef = useRef<ModalRef<'modal', FormData, SubmitResult, {}, undefined, CancelPayload>>(null);
useModalRef<FormData, SubmitResult, {}, undefined, CancelPayload>(cancelRef, defaultData);

// VK: infer from options.visibleKey literal (ref VK must match inferred value)
const drawerRef = useRef<ModalRef<'drawer', Data, void, {}, 'open'>>(null);
const { modal: drawer } = useDrawerRef(drawerRef, data, { visibleKey: 'open' });
void drawer.props.open;

Extending overlay types (module augmentation)

// types/use-modal-ref.d.ts
import type { ModalTypeItem } from 'use-modal-ref';

declare module 'use-modal-ref' {
  interface ModalTypeRegistry {
    sidePanel: ModalTypeItem;
  }
}

// app/setup.ts
import { mergeModalType, useCommonRef } from 'use-modal-ref';

mergeModalType({
  sidePanel: { visible: 'show', onClose: 'hide' },
});

const ref = React.createRef<ModalRef<'sidePanel', { title: string }, boolean>>();
const { modal } = useCommonRef('sidePanel', ref, { title: 'Panel' });

Type Safety

interface ModalData {
  title: string;
  label: string;
}

interface ModalResult {
  value: string;
}

const TestModal = React.forwardRef((props, ref) => {
  const { modal, data } = useModalRef<ModalData, ModalResult>(ref, {
    title: 'Default Title',
    label: 'Default Label'
  });
  
  // TypeScript will ensure data conforms to ModalData
  // and modal callbacks return ModalResult
});

Strict Mode Support

The library fully supports React's strict mode and concurrent features.

⚠️ Error Handling

Handle potential errors in your modal interactions:

const showModal = async () => {
  try {
    const result = await modalRef.current.modal(data);
    if (result !== undefined) {
      // Handle success
      console.log('Modal result:', result);
    } else {
      // Handle cancel/dismiss
      console.log('Modal was cancelled');
    }
  } catch (error) {
    // Handle errors during modal operations
    console.error('Modal error:', error);
  }
};

⚡ Performance Optimization

Dependency Array Usage

Use the dependency array to optimize re-renders:

const { modal, data } = useModalRef(ref, defaultData, options, [dep1, dep2]);

Lazy Initialization

For expensive initializations, use function form of defaultData:

const { modal, data } = useModalRef(ref, () => {
  // Expensive computation only runs when needed
  return {
    title: getLocalizedTitle(),
    items: generateInitialItems()
  };
});

🎯 Advanced Features

Custom Modal Types

Register extended overlay types via ModalTypeRegistry augmentation + mergeModalType (see module augmentation). You can also merge runtime aliases for built-in keys:

import { useCommonRef, mergeModalType } from 'use-modal-ref';

mergeModalType({
  tooltip: {
    visible: 'open',
    onClose: 'onOpenChange',
  }
});

const useTooltipRef = (ref, defaultData, options, deps = []) =>
  useCommonRef('tooltip' as any, ref, defaultData, options, deps);

Before/After Hooks

const { modal, data } = useModalRef(ref, defaultData, {
  beforeModal: async (data) => {
    // Called before modal opens
    console.log('Opening modal with data:', data);
    return data; // Can modify data
  },
  afterModal: (result) => {
    // Called after modal closes
    console.log('Modal closed with result:', result);
  }
});

🤝 Contributing

We welcome all contributions! Here's how you can help:

  1. Report bugs - Use the issue tracker to report bugs
  2. Suggest features - Propose new features or improvements
  3. Submit PRs - Fix bugs or implement new features

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

❓ FAQ

Why use refs instead of state for modal control?

Refs provide direct access to modal methods without triggering re-renders, making the API more efficient and easier to use.

How to handle multiple modals?

Each modal should have its own ref:

function App() {
  const modal1Ref = useRef(null);
  const modal2Ref = useRef(null);
  
  return (
    <>
      <MyModal ref={modal1Ref} />
      <AnotherModal ref={modal2Ref} />
    </>
  );
}

Can I use this with other UI libraries?

Yes! The library is UI-agnostic. Here's an example with Material-UI:

import { Dialog, DialogTitle, DialogContent } from '@mui/material';
import useModalRef from 'use-modal-ref';

const MaterialModal = React.forwardRef((props, ref) => {
  const { modal, data } = useModalRef(ref, { title: 'Dialog' });
  
  return (
    <Dialog {...modal.props} open={modal.props.visible}>
      <DialogTitle>{data.title}</DialogTitle>
      <DialogContent>
        {/* Your content */}
      </DialogContent>
    </Dialog>
  );
};