react-modal、react-modal-hook 和 react-modal-promise 都是用于在 React 应用中创建和管理模态框(Modal)的工具库,但它们在设计理念、使用方式和适用场景上有显著差异。react-modal 是一个功能完整的受控组件库,提供高度可定制的模态框实现;react-modal-hook 基于 React Hook 封装,强调声明式调用和状态管理;react-modal-promise 则专注于将模态框行为转化为 Promise,适用于需要同步用户交互结果的场景。
在 React 应用中,模态框(Modal)是常见的 UI 元素,用于确认操作、展示信息或收集输入。虽然可以手写实现,但使用专用库能更好地处理焦点管理、键盘交互、无障碍支持等细节。react-modal、react-modal-hook 和 react-modal-promise 提供了三种不同思路的解决方案。下面我们从实际开发角度深入比较。
react-modal 是一个传统的受控 React 组件。你需要显式传入 isOpen 状态,并通过回调控制其显示与隐藏。
import ReactModal from 'react-modal';
function App() {
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
<>
<button onClick={() => setModalIsOpen(true)}>打开</button>
<ReactModal
isOpen={modalIsOpen}
onRequestClose={() => setModalIsOpen(false)}
contentLabel="示例模态框"
>
<p>内容</p>
<button onClick={() => setModalIsOpen(false)}>关闭</button>
</ReactModal>
</>
);
}
react-modal-hook 将模态框封装为自定义 Hook,通过返回的函数触发显示,内部自动管理状态。
import { useModal } from 'react-modal-hook';
function App() {
const [showModal, hideModal] = useModal(() => (
<div className="modal">
<p>内容</p>
<button onClick={hideModal}>关闭</button>
</div>
));
return <button onClick={showModal}>打开</button>;
}
react-modal-promise 则将模态框视为一个“函数调用”,返回一个 Promise,在用户操作后 resolve 或 reject。
import { modalPromise } from 'react-modal-promise';
function ConfirmModal({ isOpen, onResolve, onReject }) {
if (!isOpen) return null;
return (
<div className="modal">
<p>确定要删除吗?</p>
<button onClick={() => onResolve(true)}>确定</button>
<button onClick={() => onReject()}>取消</button>
</div>
);
}
// 使用
async function handleDelete() {
try {
const result = await modalPromise(ConfirmModal);
console.log('用户确认:', result);
} catch {
console.log('用户取消');
}
}
react-modal 要求你自行维护 isOpen 状态,适合需要精细控制模态框生命周期的场景(例如动画结束后再卸载)。react-modal-hook 在 Hook 内部管理状态,调用 showModal() 时自动挂载组件,hideModal() 时卸载。减少了状态变量的声明,但牺牲了部分控制权。react-modal-promise 完全隐藏状态管理,由库内部处理模态框的挂载/卸载,并在 Promise settle 后自动清理。react-modal 通过 props 传递数据,交互通过回调函数完成。
<ReactModal isOpen={isOpen}>
<UserForm user={currentUser} onSubmit={handleSubmit} />
</ReactModal>
react-modal-hook 在定义模态内容时捕获闭包中的变量,或通过参数传递。
const [showEditModal] = useModal(({ user }) => (
<UserForm user={user} />
));
// 调用时传参
showEditModal({ user: selectedUser });
react-modal-promise 通过 onResolve/onReject 传递结果,调用方通过 await 获取值。
const result = await modalPromise(FormModal, { initialValues });
// result 是表单提交的数据
react-modal 内置完整的 WAI-ARIA 支持,包括 aria-modal、焦点陷阱、ESC 关闭等。你只需设置 appElement 即可启用。
ReactModal.setAppElement('#root');
react-modal-hook 本身不提供模态框容器,依赖你提供的 JSX。因此无障碍支持需自行实现(例如使用 dialog 元素或添加 ARIA 属性)。
react-modal-promise 同样不强制 UI 结构,无障碍能力取决于你编写的模态组件。
react-modal 和 react-modal-hook 的错误处理遵循 React 标准:组件内错误可通过 Error Boundary 捕获。react-modal-promise 将用户取消操作视为 rejection,便于统一错误处理:try {
const data = await modalPromise(InputModal);
// 处理提交
} catch (e) {
// 用户点击取消或关闭
}
react-modal 当:react-modal-hook 当:react-modal-promise 当:截至 2024 年,三个包均未被官方标记为废弃,仍在维护中。但需注意:
react-modal 要求设置 appElement 以确保无障碍正确工作,否则会发出警告。react-modal-hook 和 react-modal-promise 不提供默认样式或容器,需自行实现遮罩层、定位等 UI 细节。react-modal-promise 的 Promise 行为意味着模态框关闭后无法再更新状态(因为组件已卸载),不适合需要多次交互的场景。在大型应用中,可组合使用这些方案:
react-modal 实现基础模态容器(带 a11y 支持)react-modal-hook 封装常用业务模态(如通知、简单表单)react-modal-promise 处理关键确认流(如支付、删除)最终,选择哪个库取决于你的交互模型:是“状态驱动”、“声明触发”还是“结果等待”。理解这一点,就能在正确场景选用正确工具。
选择 react-modal-hook 如果你偏好基于 Hook 的声明式 API,希望减少样板代码,并通过组合式逻辑复用模态框行为。它适合中等复杂度的应用,尤其当你已经采用函数式组件和 Hook 架构时,能提升开发体验和代码简洁性。
选择 react-modal 如果你需要一个成熟、灵活且完全受控的模态框组件,支持无障碍(a11y)、焦点管理、自定义样式和复杂布局。它适合大型应用或对可访问性和定制性有严格要求的项目,但需要你手动管理打开/关闭状态和生命周期。
选择 react-modal-promise 如果你的模态框主要用于获取用户确认或输入(如确认删除、表单填写),并希望以 Promise 方式处理结果。它简化了异步交互流程,适合需要链式调用或与 async/await 集成的场景,但不适合复杂的非阻塞式模态交互。
Syntactic sugar for handling modal windows using React Hooks.
This library does not provide any UI, but instead offers a convenient way to render modal components defined elsewhere.
For a simple modal component check out react-modal, which works well with this library.
npm install --save react-modal-hook
Use ModalProvider to provide modal context for your application:
import React from "react";
import ReactDOM from "react-dom";
import { ModalProvider } from "react-modal-hook";
import App from "./App";
ReactDOM.render(
<ModalProvider>
<App />
</ModalProvider>,
document.getElementById("root")
);
Call useModal with the dialog component of your choice. Example using react-modal:
import React from "react";
import ReactModal from "react-modal";
import { useModal } from "react-modal-hook";
const App = () => {
const [showModal, hideModal] = useModal(() => (
<ReactModal isOpen>
<p>Modal content</p>
<button onClick={hideModal}>Hide modal</button>
</ReactModal>
));
return <button onClick={showModal}>Show modal</button>;
};
Second argument to useModals should contain an array of values referenced inside the modal:
const App = () => {
const [count, setCount] = useState(0);
const [showModal] = useModal(
() => (
<ReactModal isOpen>
<span>The count is {count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</ReactModal>
),
[count]
);
return <button onClick={showModal}>Show modal</button>;
};
Modals are also functional components and can use react hooks themselves:
const App = () => {
const [showModal] = useModal(() => {
const [count, setCount] = useState(0);
return (
<ReactModal isOpen>
<span>The count is {count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</ReactModal>
);
});
return <button onClick={showModal}>Show modal</button>;
};
Use TransitionGroup as the container for the modals:
import React from "react";
import ReactDOM from "react-dom";
import { ModalProvider } from "react-modal-hook";
import { TransitionGroup } from "react-transition-group";
import App from "./App";
ReactDOM.render(
<ModalProvider rootComponent={TransitionGroup}>
<App />
</ModalProvider>,
document.getElementById("root")
);
When TransitionGroup detects of one of its children removed, it will set its in prop to false and wait for onExited callback to be called before removing it from the DOM.
Those props are automatically added to all components passed to useModal. You can can pass them down to CSSTransition or modal component with transition support.
Here's an example using Material-UI's Dialog:
import React from "react";
import { useModal } from "react-modal-hook";
import { Button, Dialog, DialogActions, DialogTitle } from "@material-ui/core";
const App = () => {
const [showModal, hideModal] = useModal(({ in: open, onExited }) => (
<Dialog open={open} onExited={onExited} onClose={hideModal}>
<DialogTitle>Dialog Content</DialogTitle>
<DialogActions>
<Button onClick={hideModal}>Close</Button>
</DialogActions>
</Dialog>
));
return <Button onClick={showModal}>Show modal</Button>;
};
MIT © mpontus