use-modal-ref
v1.0.1
Published
react hooks for modal usage
Downloads
421
Maintainers
Readme
use-modal-ref
🚀 Powerful React hooks for elegant modal/drawer management
💡 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
visibleand 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 thathandleSubmitcannotawaitthat result—the next step must live inhandleConfirmOk, thenhandleEditOk.
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—theasynchandler stops at thatawait; noundefinedcheck 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
visibleflags 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/runtimeonly; 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-refyarn add use-modal-refpnpm add use-modal-refLocal 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 demoDemo deps and scripts live in the root
package.json. Vite aliases tosrc/for hot reload. Runyarn buildand 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.0incorrectly published CJS under bothes/andesm/whileexports.importpointed at CJS; that broke default-import interop in Rolldown/Vite (default is not a function). Fixed in1.0.1with 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 isfalse, 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 onlytypeof 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 openmodal.props.onCancel— wired tomodal.cancelModal()(the awaitedmodal()Promise rejects on cancel)
Runnable demo:
yarn demo→ Modal props binding (examples/src/demos/ModalPropsBindingDemo.tsx, componentexamples/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.propsis{ 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 overwritedefaultData.title(explicitundefinedis skipped when merging open payload).- Confirm with
modal.endModal(value); cancel via overlay close /modal.props.onCancel→cancelModal→ 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 demo→ 7. JavaScript + JSDoc (#/usage/js-radio-modal); sourcesexamples/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@typedefonce in the modal component file; callers referenceimport('./RadioModal').RadioModalRef/RadioModalDatawithout 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 modaldata(defaults toany)U = any- Promise resolve type frommodal()C extends Record<string, any> = {}- Type of custom properties inoptions.custom, merged ontoModalRef
Parameters:
ref: React.Ref<any>- React ref object for controlling modal show/hide. Must be passed to the modal component viaReact.forwardRefdefaultData?: 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 neededoptions?: ModalRefOption<'modal', T, U, C>- Hook config and lifecycle callbacks; see ModalRefOptiondeps?: React.DependencyList- When deps change, re-syncs lifecycle callbacks andcustominternally (does not changemodalidentity; lifecycle hooks are not on the publicmodalAPI—configure them viaoptions)
Returns:
modal: ModalRef<'modal', T, U, C>- Modal control object with:visible: boolean- Read-only visibility flagdata: T- Read-only current modal dataprops: { visible: boolean, onCancel: () => void }- Spread onto Ant DesignModal(visible/onCancel)options: ModalRefOption<'modal', T, U, C>- Read-only merged hook optionsmodalOptions: ModalModalOptions- Read-only runtime options from the latestmodal()callmodalPromise: Promise<U> | null- Read-only current Promise,nullwhen closedmodal(newData?: Partial<T>, options?: ModalModalOptions): Promise<U>- Open modal and return PromiseendModal(result?: U, onDone?: () => void): Promise<void>- Confirm close and resolve PromisecancelModal(ex?: any, onDone?: () => void): Promise<void>- Cancel; rejects unlessalwaysResolveis set
data: T- Current modal data (same asmodal.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 viaoptionsonly—they are not part of the publicmodal/ref.currenttype.
useImperativeHandle?: boolean- Whether to exposemodalonrefviauseImperativeHandle. Defaults totrue(onlyfalsedisables it). Treat as constant—do not change at runtime after the first renderalwaysResolve?: boolean- Whentrue,cancelModalresolves the Promise instead of rejectingvisibleKey?: string- Overrides the default visibility prop name frommodalTypeMap, producingmodal.props.[visibleKey]. For Ant Design 5Drawer, use'open'; see mergeModalType for global mappingcustom?: C- Custom properties merged ontomodal, accessible viamodal.xxxorref.current.xxx. Must not use built-in modal property names (e.g.modal,visible,endModal,beforeModal); conflicting keys are ignored with a dev warningcustom 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 returnPartial<T>to merge into data; callpause(result, isError?)to abort (isError: truethrows)newData- Data passed to thismodal()callpause- Abort/pause callbackoptions- Runtime options for thismodal()call
afterModal?: (newData, options?) => void | Promise<void>- Post-open hook (recommended). Hook-leveloptions.afterModaland per-callmodal(data, { afterModal })each fire once and do not replace each otherinit?: (newData, options) => void | Promise<void>- Legacy post-open callback (older API); kept for compatibility—preferafterModalfor new code. If both are set,initruns first, thenafterModalbeforeCloseModal?: (next, action, modal) => void | Promise<void>- Pre-close hook; callnext(false)to prevent closingaction-'end'(confirm) or'cancel'
afterCloseModal?: (newData, action, modal) => void | Promise<void>- Post-close hook; return value becomes newdata
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 5Drawerusingopen:
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.visibleKeyinstead 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/hidedefaultData?: Partial<T> | (() => Partial<T>)- Default data (object or lazy factory)options?: ModalRefOption<'drawer', T, U, C>- Hook config; see ModalRefOptiondeps?: React.DependencyList- Re-syncoptionsontomodalwhen deps change
Returns:
modal: ModalRef<'drawer', T, U, C>- Drawer control object; defaultmodal.propsis{ visible, onClose }(map to{ open, onClose }viamergeModalTypeorvisibleKey)data: T- Current drawer datasetData: (newData: T | ((data: T) => T)) => void- Update drawer data
With Ant Design Drawer: Built-in
drawerexposesmodal.props.visibleby default. Ant Design 5+Drawerexpectsopen—callmergeModalType({ drawer: { visible: 'open', onClose: 'onClose' } })at app entry, or setvisibleKey: 'open'in hookoptionsfor 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 viamergeModalType)T,U,C- Same as useModalRef
Parameters:
modalType: P- Overlay type identifierref: React.Ref<any>- React ref for show/hide controldefaultData?: Partial<T> | (() => Partial<T>)- Default data (object or lazy factory)options?: ModalRefOption<P, T, U, C>- Hook config; see ModalRefOptiondeps?: React.DependencyList- Re-syncoptionsontomodalwhen deps change
Returns:
modal: ModalRef<P, T, U, C>- Overlay control objectdata: T- Current datasetData: (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 withReact.forwardRefmodalData?: D- Data to pass to the modal, will be passed as the first argument to themodalmethodoptions?: object- Configuration options object with the following properties:props?: Record<string, any>- Props to pass to the componentmodalMethod?: 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 themodalmethodcloseWhenUrlChange?: boolean- Whether to automatically close modal when URL changes, defaults tofalseselector?: string- Container selector for finding existing container elementid?: string- Container ID for finding or creating container elementcontainer?: HTMLElement | null | (() => HTMLElement)- Container element or function returning containerclassName?: string- Container class name, only used when creating new containeronRef?: (ref: any, destroy: () => void) => void- Callback function after ref is createdonAppendContainer?: (container: HTMLElement) => void | boolean- Callback before container is appended to DOM, returntrueto prevent auto appendonRemoveContainer?: (container: HTMLElement) => void | boolean- Callback before container is removed from DOM, returntrueto prevent auto removeonDestroyComponent?: (container: HTMLElement) => void | boolean- Callback before component is destroyed, returntrueto prevent auto destroydestroyDelay?: number- Destroy delay in milliseconds, defaults to50
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 withReact.forwardRefprops?: P | null- Props to pass to the componentoptions?: 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 oneid?: string- Container ID for finding or creating container element. If found, will use that container; otherwise creates a new one and sets this IDcontainer?: HTMLElement | null | (() => HTMLElement)- Container element or function returning container. If provided, will use this container directlyclassName?: string- Container class name, only used when creating new containeronRef?: (ref: any, destroy: () => void) => void- Callback function after ref is created, receives ref object and destroy functiononAppendContainer?: (container: HTMLElement) => void | boolean- Callback function before container is appended to DOM, returntrueto prevent auto append todocument.bodyonRemoveContainer?: (container: HTMLElement) => void | boolean- Callback function before container is removed from DOM, returntrueto prevent auto removeonDestroyComponent?: (container: HTMLElement) => void | boolean- Callback function before component is destroyed, returntrueto prevent auto call toReactDOM.unmountComponentAtNodedestroyDelay?: number- Destroy delay in milliseconds, defaults to50. 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 componentdestroy: () => 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 visibledata: T- Read-only property containing the current modal data objectprops: 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 genericVK(e.g.ModalRef<'drawer', T, U, C, 'open'>) aftermergeModalTypeorvisibleKey: 'open' - For
'popover'type:{ visible: boolean, onClose: () => void }
- For
options: ModalRefOption<P, T, U, C>- Read-only property containing current configuration optionsmodalOptions: ModalModalOptions- Read-only property containing current modal optionsmodalPromise: Promise<U> | null- Read-only property containing current modal Promise,nullif modal is not open
Method Descriptions:
modal(newData: T, options?: ModalModalOptions): Promise<U>- Method to open the modalnewData: T- New data to pass to the modal, will be merged withdefaultDataoptions?: ModalModalOptions- Modal options with the following properties:modalDataEvent?: boolean- Legacy data-payload callbacks (optional). Whentrue, you may passonOK/onCancelon thenewDataobject ofmodal(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 thanundefined, that becomes theawait modal()resolve value (beforebeforeEndModal). - On
cancelModal(reason),onCancel(reason)runs similarly and may rewrite the reject reason (beforebeforeCancelModal). - Field names are
onOK(capitalOK) andonCancel— not Ant Design’sonOk. - New code: prefer
await modal()+beforeEndModal/beforeCancelModalinmodal()options; usemodalDataEventonly 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'- On open, those keys are removed from
afterModal?: (newData: any, options?: ModalModalOptions) => void | Promise<void>- Hook after modal opensbeforeCloseModal?: (next: (ok: any) => void, action: ModalAction) => void | Promise<void>- Hook before closebeforeEndModal?: ModalBeforeEndHandler<U>- Pre-end hook; may rewrite the resolved value. Receives currentUfromendModal, returnsU | undefined | voidbeforeCancelModal?: (reason?: any) => Promise<any>- Hook before cancel, can modify cancel reasonafterCloseModal?: (newData: any, action: ModalAction, modal: ModalRef<any, any, any>) => void | Promise<void>- Hook after closeforceVisible?: boolean- Force show, even if another modal is already openalwaysResolve?: 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 modalresult?: U- Result value to returnonDone?: () => void- Callback function after completion
cancelModal(ex?: any, onDone?: () => void): Promise<void>- Cancels the modalex?: 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 listenerlistener: (url: string) => void- Listener function called when URL changes, receives new URL as parameteroptions?: { once?: boolean }- Options object, whenonceistrue, 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:
- Report bugs - Use the issue tracker to report bugs
- Suggest features - Propose new features or improvements
- 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>
);
};