mui-modal-context
v3.0.0
Published
Imperative MUI Dialog via a React hook, while keeping the dialog inside the caller's context tree.
Maintainers
Readme
mui-modal-context
通过 React Hook 命令式打开 MUI Dialog,同时把对话框挂在调用方的 React 组件树里 —— 保留 Theme / I18n / Store 等上下文。
English | 简体中文
✨ 特性
- 🎯 命令式 API:
useModal()→[modal, holder],像调函数一样开关 Dialog - 🌳 保留上下文:Dialog 渲染在调用组件的子树里,自动继承
ThemeProvider、i18n、Redux 等所有上下文 - ⚡ React 18 / 19 全兼容:自动 JSX 运行时,无需引入
React - 🧩 MUI 5+ / 6+ / 7+ / 9+ 都能用:peer range 设为
>=5.11,只依赖稳定的DialogAPI - 🔒 严格 TypeScript:全程
strict: true,零any、零as—— ESLint 强制 - 📦 现代化打包:Vite 8 库模式,产出 ESM + CJS + 类型声明,
sideEffects: false,tree-shake 友好
📦 安装
pnpm add mui-modal-context
# 或
npm install mui-modal-context
# 或
yarn add mui-modal-context需要本地已安装以下 peer 依赖:
pnpm add react react-dom @mui/material @emotion/react @emotion/styled| peer | 支持版本 |
| --------------- | ---------- |
| react | >=18.0.0 |
| react-dom | >=18.0.0 |
| @mui/material | >=5.11.0 |
| @emotion/react | >=11.10.0|
| @emotion/styled | >=11.10.0|
🚀 快速上手
import { useModal } from 'mui-modal-context'
import { DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'
export default function App() {
const [modal, contextHolder] = useModal()
const openDialog = () => {
const ref = modal({
maxWidth: 'sm',
fullWidth: true,
children: (
<>
<DialogTitle>Hello</DialogTitle>
<DialogContent>这个 Dialog 拿得到外层的 Theme / Store / i18n。</DialogContent>
<DialogActions>
<Button onClick={() => ref.destroy()}>关闭</Button>
</DialogActions>
</>
)
})
}
return (
<>
<Button onClick={openDialog}>打开</Button>
{contextHolder}
</>
)
}重要:
contextHolder必须被渲染在你的 JSX 里(任意位置都行),否则 Dialog 不会显示 —— 这正是 hook 能继承外层上下文的关键。
🔧 更多用法
更新已打开的 Dialog
const ref = modal({
children: <DialogContent>Loading...</DialogContent>
})
// 异步拿到数据后更新内容
fetchData().then((data) => {
ref.update({
children: (
<>
<DialogTitle>{data.title}</DialogTitle>
<DialogContent>{data.body}</DialogContent>
</>
)
})
})继承外层 Context (这是核心卖点)
<ThemeProvider theme={darkTheme}>
<I18nProvider locale="zh">
<Provider store={store}>
<App />
</Provider>
</I18nProvider>
</ThemeProvider>App 里通过 useModal() 打开的 Dialog 会自动用 darkTheme、读到 zh 语言包、能 useSelector() 拿 store —— 因为它就挂在 App 的子树里,而不是被 Portal 到根节点之外的另一棵树。
Next.js App Router
源码顶部已经加了 'use client',在 Server Component 里直接 import 即可,无需自己包一层 client wrapper。
📖 API
useModal()
function useModal(): [modal: modalContentType, holder: ReactElement]返回元组:
modal(config)— 打开一个 Dialog,返回{ destroy, update }holder— 必须渲染到 JSX 中的承载节点
modal(config)
type ModalConfig = Omit<DialogProps, 'open'> // 接受所有 MUI DialogProps,除了 open(由库内部控制)
interface ModalHandle {
destroy: () => void // 关闭并销毁(走完 transitionDuration.exit 动画)
update: (cfg: ModalConfig) => void // 浅合并 config(改 title / content / props 等)
}可以在 modal() 返回之后立刻调用 destroy() 或 update() —— 内部有 action queue 兜底,在子组件挂载完成后再执行。
如果你在 config 里自定义了 onClose,会覆盖内部的关闭逻辑(此时关闭由你接管,通常需要自己保存 ref 并调用 ref.destroy())。
🏗️ 架构 (一句话)
useModal 维护一个 ElementsHolder,每次调 modal(config) 就把一个新的 HookModal(包装 @mui/material 的 Dialog)塞进 holder 的元素列表;HookModal 通过 useImperativeHandle 暴露 destroy / update,关闭时按 transitionDuration 等动画结束再从列表中移除。
详见 src/index.tsx 与 src/modal.hook.tsx。
🛠️ 本地开发
pnpm install
pnpm run typecheck # tsc --noEmit
pnpm run lint # eslint . (禁 any/as)
pnpm run build # vite build → dist/🧱 技术栈
- TypeScript 5.x(strict)
- Vite 8 +
@vitejs/plugin-react+vite-plugin-dts - ESLint 9 flat config + typescript-eslint 8
- 产物: ESM
dist/index.mjs+ CJSdist/index.cjs+ 类型dist/index.d.ts
🤝 贡献
欢迎 Issue / PR。开始前请阅读 CONTRIBUTING.md。
📄 License
MIT © nvrenshiren
🙏 致谢
- 灵感来自 Ant Design
Modal.useModal()的 in-context 实现思路 - 基于 Material UI
Dialog组件
