React useDisclosure Hook:管理模态框和抽屉的打开关闭状态 (2026)

每个 React 应用都会逐渐积累各种可切换的 UI------确认对话框、移动端导航抽屉、设置弹出框、通知面板。它们背后的状态始终相同:一个布尔值、一个打开方法、一个关闭方法,可能再加一个状态变化时触发埋点或焦点管理的回调。于是你写了 useState(false) 加三个内联处理函数,复制粘贴到下一个模态框,到第五个可切换组件的时候,你发现同样的五行模式散落在十几个文件里,没有复用,也没有生命周期钩子。

useDisclosure(来自 @reactuses/core)将这一模式提取为一次性解决方案:默认非受控,需要时可切换为受控模式,提供 onOpen / onClose / onChange 回调在恰当的时机触发。返回的处理函数通过 ref 实现引用稳定,不会导致子组件不必要的重渲染。本文介绍 API、内部实现、受控与非受控的契约,以及模态框、抽屉和组合式多重 disclosure UI 的实际模式。TypeScript 优先。

最简单的用法:模态框切换

tsx 复制代码
import { useDisclosure } from '@reactuses/core';

function App() {
  const { isOpen, onOpen, onClose } = useDisclosure();

  return (
    <>
      <button onClick={onOpen}>打开设置</button>
      {isOpen && (
        <dialog open>
          <h2>设置</h2>
          <p>这里是设置面板内容。</p>
          <button onClick={onClose}>关闭</button>
        </dialog>
      )}
    </>
  );
}

不需要 useState,不需要写内联的 () => setOpen(true) / () => setOpen(false),不需要纠结命名。Hook 返回语义明确的具名函数------触发器上用 onOpen,关闭按钮上用 onClose。每次渲染返回相同的函数引用(ref 稳定化),所以把 onClose 传给 React.memo 包裹的子组件也不会破坏优化。

完整 API

ts 复制代码
const {
  isOpen,       // boolean --- 当前状态
  onOpen,       // () => void --- 设为 true
  onClose,      // () => void --- 设为 false
  onOpenChange, // () => void --- 切换:关闭时调用 onOpen,打开时调用 onClose
  isControlled, // boolean --- 如果传了 isOpen prop 则为 true
} = useDisclosure({
  defaultOpen,  // boolean --- 初始状态(仅非受控模式)
  isOpen,       // boolean --- 传入以进入受控模式
  onOpen,       // () => void --- 打开后触发
  onClose,      // () => void --- 关闭后触发
  onChange,     // (isOpen: boolean | undefined) => void --- 任何变化时触发
});

所有字段都是可选的。不传任何参数调用 useDisclosure() 就能得到一个初始关闭的非受控切换,覆盖大多数模态框和抽屉的需求。

生命周期回调:当打开和关闭有副作用时

布尔切换不够用的时刻,就是你的模态框不只是显示和隐藏的时刻。真实的 disclosure 组件需要副作用:用户打开定价弹窗时发送埋点事件,抽屉打开时捕获焦点,关闭时恢复焦点。内联处理函数会把这些逻辑分散到 JSX 各处:

tsx 复制代码
// 没有 useDisclosure 时------副作用与 JSX 缠在一起
<button onClick={() => {
  setIsOpen(true);
  analytics.track('pricing_modal_opened');
  focusTrap.activate();
}}>
  查看定价
</button>

使用 useDisclosure,副作用集中在 Hook 调用处:

tsx 复制代码
const { isOpen, onOpen, onClose } = useDisclosure({
  onOpen() {
    analytics.track('pricing_modal_opened');
    focusTrap.activate();
  },
  onClose() {
    analytics.track('pricing_modal_closed');
    focusTrap.deactivate();
  },
});

// JSX 变得简洁
<button onClick={onOpen}>查看定价</button>

回调在状态更新之后 触发。回调 props 内部通过 useLatest 包装------你可以传入内联箭头函数而不会导致返回的 onOpen / onClose 获得新的引用。

受控模式:由父组件掌控状态

有时打开状态属于父组件或状态管理器。传入 isOpen prop,Hook 就会切换到受控模式:

tsx 复制代码
function ControlledDrawer({ isOpen, onToggle }: Props) {
  const disclosure = useDisclosure({
    isOpen,
    onOpen: onToggle,
    onClose: onToggle,
  });

  // disclosure.isControlled === true
  // disclosure.isOpen 反映 prop 的值
  // disclosure.onOpen / onClose 触发父组件的 onToggle

  return (
    <aside className={disclosure.isOpen ? 'open' : ''}>
      <button onClick={disclosure.onClose}>×</button>
      {/* 抽屉内容 */}
    </aside>
  );
}

受控模式下,onOpenonClose 不会 更新内部状态------Hook 尊重 prop 作为数据源。两种模式的边界很清晰:isOpenundefined → 非受控。isOpen 是布尔值 → 受控。

onOpenChange:切换简写

useDisclosure 返回的 onOpenChange 函数就是一个切换器:disclosure 关闭时调用 onOpen,打开时调用 onClose。直接映射到暴露单一回调的组件:

tsx 复制代码
const { isOpen, onOpenChange } = useDisclosure();

// 适配 Radix 风格的 API
<Dialog.Root open={isOpen} onOpenChange={onOpenChange}>
  <Dialog.Trigger>打开</Dialog.Trigger>
  <Dialog.Content>...</Dialog.Content>
</Dialog.Root>

内部实现

完整实现很简短,三个构建模块:

  1. useControlled --- 在内部 useState 和外部 prop 之间切换的 Hook。
  2. useLatest --- 把回调 props 包装在 ref 中,使返回的处理函数引用稳定。
  3. 受控守卫 --- if (!isControlled) setIsOpen(...) 确保 Hook 不会与父组件的状态冲突。

没有 effect,没有订阅,没有浏览器 API。Hook 天然 SSR 安全------纯 React 状态。

useDisclosure vs useBoolean vs useToggle

@reactuses/core 有三个管理布尔值的 Hook:

useDisclosure useBoolean useToggle
受控模式 支持 不支持 不支持
生命周期回调 onOpenonCloseonChange
处理函数稳定性 通过 useLatest ref 稳定化 标准 useCallback 标准 useCallback
最适合 模态框、抽屉、弹出框 简单显示/隐藏标志 极简布尔切换

如果不需要回调或受控模式,useBooleanuseToggle 更轻量。

实际模式

确认对话框

tsx 复制代码
function DeleteButton({ onConfirm }: { onConfirm: () => void }) {
  const { isOpen, onOpen, onClose } = useDisclosure();

  return (
    <>
      <button onClick={onOpen}>删除</button>
      {isOpen && (
        <div className="overlay" onClick={onClose}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <p>确定要删除吗?</p>
            <button onClick={() => { onConfirm(); onClose(); }}>
              是的,删除
            </button>
            <button onClick={onClose}>取消</button>
          </div>
        </div>
      )}
    </>
  );
}

多个 Disclosure 互斥

tsx 复制代码
function SettingsPanel() {
  const general = useDisclosure({ defaultOpen: true });
  const security = useDisclosure();
  const notifications = useDisclosure();

  const closeAll = () => {
    general.onClose();
    security.onClose();
    notifications.onClose();
  };

  const openExclusive = (target) => {
    closeAll();
    target.onOpen();
  };

  return (
    <div>
      <button onClick={() => openExclusive(general)}>常规</button>
      <button onClick={() => openExclusive(security)}>安全</button>
      <button onClick={() => openExclusive(notifications)}>通知</button>

      {general.isOpen && <GeneralSettings />}
      {security.isOpen && <SecuritySettings />}
      {notifications.isOpen && <NotificationSettings />}
    </div>
  );
}

每个区段有自己的 useDisclosureopenExclusive 辅助函数先关闭所有,再打开一个------不需要手风琴库就能实现手风琴行为。

从 Chakra UI 迁移

API 几乎一样。主要区别:

  • 没有 getButtonProps / getDisclosureProps --- 管理状态,不管理 DOM 属性。
  • onOpenChange 而非 onToggle --- 行为相同,与 Radix/Headless UI 命名一致。
  • 暴露 onChange 回调用于同步到外部 store。
  • 不依赖 UI 框架。

迁移就是一次重命名。

要点总结

  • useDisclosure 替代了 useState(false) + 三个内联处理函数的模式------你的每个模态框、抽屉、弹出框里都有的那个。
  • 生命周期回调集中管理副作用------埋点、焦点管理、动画触发。
  • 受控模式可选 :传入 isOpen,Hook 听从你的状态;不传,Hook 自己管理。
  • 处理函数引用稳定------安全传给 memo 化的子组件。
  • onOpenChange 是切换函数,直接映射到 Radix/Headless UI/Ariakit 的单回调 API。
  • 天然 SSR 安全------纯 React 状态。

@reactuses/core 获取,不要再复制粘贴模态框状态了。

相关推荐
学高数就犯困1 小时前
React:常见的性能优化手段
前端·react.js
天天码行空1 小时前
vkeyboardhand:零依赖虚拟键盘指法组件
前端·javascript·vue.js
濮水大叔1 小时前
为什么 AI 最擅长 React/Next.js,却很少看到真正好用的 Next.js 开源项目?
react.js·node.js·next.js
我要两颗404西柚1 小时前
Stage four:VUE项目上线
前端·javascript·vue.js
Cloud_bread4 小时前
从ReAct到自主闭环:Agentic Coding核心执行引擎的技术演进
前端·react.js·前端框架
满栀5854 小时前
原生JS + ECharts 多图表可视化实战:从业务实现到工程化优化
开发语言·javascript·echarts
sugar__salt6 小时前
Vue3 自定义指令与插槽(Slot)技术详解
前端·javascript·vue.js·前端框架·vue
Darling噜啦啦7 小时前
React 组件进化论:从状态混乱到 UI = fn(props) 的三次重构
react.js
Hilaku7 小时前
前端真的比后端简单吗?
前端·javascript·程序员