跳到主要内容

Confirm 确认对话框

用于等待用户确认的操作。confirm() 返回 Promise<boolean>,使用前挂载 ConfirmHost

Promise 必然 settle

confirm() 在任何路径下都会 resolve,不会悬挂 —— 包括没挂 Host、重入、Host 渲染抛错、Host 卸载和被后挂载的 Host 接管。详见下方生命周期契约

用法

import { confirm } from '@unif/react-native-design';

const ok = await confirm({
title: '确认注销账号?',
message: '注销后所有数据将被删除,且无法恢复。',
confirmLabel: '确认注销',
destructive: true, // 红色按钮 c.error
});
if (ok) doLogout();

API

参数类型默认值说明
titlestring主标题(必传),短句:确认注销账号? / 确认退出登录?
messagestring?说明文本(可选)1-2 句解释操作后果
confirmLabelstring?'确认'确认按钮文案
cancelLabelstring?'取消'取消按钮文案
destructiveboolean?false标记破坏性操作 → 确认按钮变红(c.error)

无障碍(a11y)

来源:src/components/ui/Confirm/ConfirmHost.tsxconfirm.tstypes.ts

confirm() 是命令式 API,ConfirmOptions没有任何 a11y 入参,a11y 全部来自底层组合:

  • 对话框容器走 RN 原生 <Modal transparent animationType="slide">。backdrop 虽可供触屏用户点击取消,但显式 accessible={false}不会以“关闭”按钮进入 a11y tree,避免把标题、说明和两个 action 合并成一个节点。
  • 标题 / 说明用 <Text>entry.options.title / entry.options.message)渲染,screen reader 可读到文字。
  • 取消 / 确认两个按钮用本库 Button 渲染。自定义 cancelLabel / confirmLabel 会先 trim;空白值分别回退“取消”/“确认”,因此不会把空名称传给 Button。
  • screen reader 的取消路径是可见的取消按钮;Android 系统返回走 Modal 的 onRequestClose,同样结算为 false

要让 SR 读到清晰语义,给 title(必填)/ confirmLabel / cancelLabel 传明确文案即可;组件层无额外 a11y 旋钮。

// title 必填即标题语义;按钮文案即按钮 a11y label
await confirm({
title: '确认注销账号?',
confirmLabel: '确认注销',
destructive: true,
});

返回值

Promise<boolean>:

  • true — 用户点确认按钮
  • false — 用户点取消 / 点 backdrop / 系统返回,以及下表所有兜底路径

生命周期契约

confirm() 由一个纯状态机驱动(src/components/ui/Confirm/store.ts),两条不变量: 同一时间只有一个 Host 在收事件(后挂载者接管、卸载归还)同一时间只有一个未决对话框。 所有关闭路径汇聚到同一个 identity-guarded、幂等的 settle

场景结果说明
未挂 <ConfirmHost />立即 false + dev warn不占单例锁 —— 之后挂上 Host 仍能正常弹出
已有对话框在显示时再调用立即 false + dev warn拒绝重入,已显示的那个不受影响
挂了多个 <ConfirmHost />最后挂载的生效栈式接管:新 Host 接手事件,前任入栈挂起;前任手里未决的对话框立即 false 并关闭
接管者卸载自动归还给挂起的前任(乱序卸载也安全:挂起者先卸载只是从栈里摘掉)
Host 渲染 / 订阅回调抛错false只作废该 owner;新 Host 挂上后可正常接管
Host 卸载时对话框仍未决false由 Store 结算,Promise 不会永久悬挂
同一次对话框被 settle 两次第二次无效幂等,结果以第一次为准
旧对话框的迟到回调无效identity guard:旧 entry 引用永远匹配不上新的 active,不会误关新对话框
根上挂一个,别挂在会被条件卸载的子树里

<ConfirmHost /> 在 App 根挂一个就够。多挂的实例不会「都渲染一遍」——后挂载的接管、前任挂起,卸载再归还。所以把它挂在会被条件卸载的子树里仍然危险:卸载瞬间该 Host 手里未决的对话框会被结算为 false

在自己的 Modal 里挂第二个 —— 这是受支持的用法

RN Modal独立的 native window,根 Host 渲染的对话框会被它整块盖住(iOS 上兄弟 Modal 更是不叠放,根本看不见)。所以在 Modal 的内容树里再挂一个 <ConfirmHost />正解:它接管 owner,confirm() 渲染进 Modal 自己的 window(成为嵌套 present,iOS 支持);Modal 关闭、内层 Host 卸载后 owner 自动归还给根 Host。

接管瞬间前任手里那个未决对话框会立即 resolve(false) 并关闭 —— 这是刻意的:Promise 不悬挂、单例槽不被带走,新 Host 一上来就能弹。归还(Modal 关闭)方向同样结算成 false:confirm 的上下文随 Modal 一起消失了,当作取消处理。这一点和 Toast 不同 —— toast 归还时会把没播完的那条交回上一层重播,详见 Toast → 投递语义

接管以挂载顺序为准,不看层级。 谁最后挂载谁就是 owner —— 如果根子树被 re-key(切主题 / 切语言 / ErrorBoundary 重置)导致根 <ConfirmHost /> 重新挂载,它会从 Modal 内那个手里夺回 owner,并把当时未决的对话框结算成 false。别在 Modal 开着的时候 re-key 根。

ConfirmEntry / ConfirmEvent / lease 等标识类型是 Host 与 Store 之间的内部协议,不从包根导出,也不保证跨版本稳定。

设计稿对照

视觉态规则
容器RN <Modal transparent animationType="slide"> + backdrop,底部弹层卡片
标题t.heroSm(18)+ fw.semi + c.foreground
说明t.body(15)+ c.foregroundMuted + lineHeight 1.45
按钮行两个 <Button> 是 Confirm action row 的直接 children,并由内部 flex: 1 平分横向主轴

主题键(Tokens)

读取来源:src/components/ui/Confirm/styles.tsConfirmHost.tsx

Token来源作用
c.surfaceuseColors()sheet 背景色
c.foregrounduseColors()标题文字色
c.foregroundMuteduseColors()说明文字色
type.heroSm@unif/react-native-design标题字号(18)
type.body@unif/react-native-design说明字号(15)
fw.semi@unif/react-native-design标题字重
space['9'] / space['4'] / space['5'] / space['7']@unif/react-native-designsheet / actions 内边距与 gap

业务消费示例

  • 注销账号(AccountSecurity.tsx)— destructive: true,取消则不调用注销 API
  • 退出登录(Setting.tsx)— destructive: true,取消则保持 authed=true
  • 未来:删除会话 / 取消订单 / webview 跳转前提示(根据 UX 决策)

业务侧 loading

confirm() 立即关闭对话框 + resolve,不内置 loading。caller 自行处理:

const [loggingOut, setLoggingOut] = useState(false);

const handleLogout = async () => {
const ok = await confirm({ title: '确认退出?', destructive: true });
if (!ok) return;
setLoggingOut(true);
try {
await logout();
} catch {
setLoggingOut(false);
}
};

// 在 cell title 上展示 "退出中…" + disabled

使用注意

  • ❌ 不要嵌套 confirm(同一时间只允许 1 个,新请求被拒绝 + dev warn)
  • ❌ 不要把"信息提示"用 confirm(用 toast()),confirm 只用于"用户决策"
  • ❌ 不要为「提高可用性」乱挂多个 <ConfirmHost /> —— 只有最后挂载的那个在收事件;多挂只在 Modal 内有意义(见上方 tip)
  • ❌ 不要把 <ConfirmHost /> 挂在会被条件卸载的子树里 —— 卸载会把它手里未决的对话框结算为 false

关联组件

  • Toast— 单向反馈(不需用户决策)