指南:异步确认模态框

删除项目这类不可撤销的操作,在发送请求前需要明确确认。本指南用 await 逐步构建这个流程,让它在一个函数里从上到下读下来。

为什么用 Promise 而不是状态

在基于状态的模态框中,点击处理函数修改 isOpen,模态框在别处渲染,实际操作被移到 onConfirm 回调里。一个动作被分散到三个地方。

使用 SnappyModal.show() 时,处理函数打开模态框、等待回答,然后继续执行。局部变量保持可用,可以提前 return,一个 try/catch 就能覆盖整个流程。

tsx
1// State-driven: one action, three places
2const [isOpen, setIsOpen] = useState(false);
3<button onClick={() => setIsOpen(true)}>Delete</button>
4<ConfirmDialog open={isOpen} onConfirm={deleteProject} onCancel={() => setIsOpen(false)} />
5
6// Promise-driven: one function
7async function onDelete() {
8  if (!(await SnappyModal.show(<ConfirmDialog />))) return;
9  await deleteProject();
10}

1. 可复用的确认模态框

模态框只报告决定:确认时 resolve 为 true,取消时为 false。它不知道要删除什么,因此应用中的所有确认流程都可以复用同一个组件。

ConfirmDialog.tsxtsx
1import { useCurrentModal } from 'react-snappy-modal';
2
3type Props = { title: string; message: string; confirmLabel?: string };
4
5export function ConfirmDialog({ title, message, confirmLabel = 'Confirm' }: Props) {
6  const { resolveModal } = useCurrentModal();
7
8  return (
9    <div role="dialog" aria-modal="true" className="dialog">
10      <h2>{title}</h2>
11      <p>{message}</p>
12      <button onClick={() => resolveModal(false)}>Cancel</button>
13      <button onClick={() => resolveModal(true)}>{confirmLabel}</button>
14    </div>
15  );
16}

2. 处理所有结果

结果有三种,一次 falsy 判断即可处理两种取消方式。

用户操作结果处理
点击删除true发送请求
点击取消false中止
点击模态框外部undefined与取消一样中止
tsx
1const confirmed = await SnappyModal.show(
2  <ConfirmDialog title="Delete project?" message="This cannot be undone." confirmLabel="Delete" />,
3);
4
5if (!confirmed) return; // false (Cancel) or undefined (backdrop click)

3. 提示失败

fetch 在 HTTP 错误时不会 reject,因此需要自行检查 response.ok。请求失败时用另一个模态框显示错误,用户不会丢失上下文,函数也在同一处结束。

tsx
1async function deleteProject(project: { id: string; name: string }) {
2  const confirmed = await SnappyModal.show(
3    <ConfirmDialog
4      title={`Delete "${project.name}"?`}
5      message="This cannot be undone."
6      confirmLabel="Delete"
7    />,
8  );
9  if (!confirmed) return;
10
11  try {
12    const response = await fetch(`/api/projects/${project.id}`, { method: 'DELETE' });
13    if (!response.ok) throw new Error(`Server responded with ${response.status}`);
14  } catch (error) {
15    await SnappyModal.show(<AlertDialog message={String(error)} />);
16  }
17}

4. 保存期间保持模态框打开

有时需要在服务器响应前保持模态框打开,以显示进度或服务器错误。这时把请求移到模态框内部,只在成功后 resolve。

请求进行中禁用按钮,并传入 allowOutsideClick: false,避免外部点击中途关闭模态框。

DeleteDialog.tsxtsx
1function DeleteDialog({ projectId }: { projectId: string }) {
2  const { resolveModal } = useCurrentModal();
3  const [saving, setSaving] = useState(false);
4  const [error, setError] = useState('');
5
6  const confirm = async () => {
7    setSaving(true);
8    setError('');
9    try {
10      const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' });
11      if (response.ok) return resolveModal(true);
12      setError(`Delete failed (${response.status}). Please try again.`);
13    } catch {
14      setError('Network error. Check your connection and try again.');
15    } finally {
16      setSaving(false);
17    }
18  };
19
20  return (
21    <div className="dialog">
22      <p>Delete this project?</p>
23      {error && <p role="alert">{error}</p>}
24      <button disabled={saving} onClick={() => resolveModal(false)}>Cancel</button>
25      <button disabled={saving} onClick={confirm}>{saving ? 'Deleting…' : 'Delete'}</button>
26    </div>
27  );
28}
29
30const deleted = await SnappyModal.show(<DeleteDialog projectId={id} />, {
31  allowOutsideClick: false,
32});

5. 何时 reject 而不是 resolve

取消是正常结果,应当 resolve。rejectModal(error) 和 SnappyModal.throw(error) 只用于调用方必须处理的真正失败,并始终用 try/catch 包裹。未捕获的 rejection 会表现为 unhandled promise rejection。

tsx
1try {
2  const file = await SnappyModal.show(<UploadDialog />);
3  attach(file);
4} catch (error) {
5  // rejectModal(error) or SnappyModal.throw(error) lands here
6  report(error);
7}

6. 防止同一模态框打开两次

快速双击会让处理函数执行两次,叠加出两个相同的模态框。SnappyModal.isShow() 为 true 时直接返回;如果可能有其他模态框打开,则使用局部的进行中标志。

tsx
1async function onDelete() {
2  if (SnappyModal.isShow()) return; // a dialog is already open
3  const confirmed = await SnappyModal.show(<ConfirmDialog title="Delete?" message="…" />);
4  // ...
5}

检查清单

上线确认流程前,请检查以下各项。

  • 每个按钮都会 resolve 一个值,Promise 不会一直等待。
  • false 和 undefined 都按取消处理。
  • 除网络错误外,也用 response.ok 检查 HTTP 错误。
  • 发送请求的破坏性操作模态框使用 allowOutsideClick: false。
  • 使用 rejectModal 或 SnappyModal.throw 的地方都用 try/catch 包裹。
  • 重复点击不会打开第二个模态框。
下一篇指南:模态框层级 →