指南:异步确认模态框
删除项目这类不可撤销的操作,在发送请求前需要明确确认。本指南用 await 逐步构建这个流程,让它在一个函数里从上到下读下来。
为什么用 Promise 而不是状态
在基于状态的模态框中,点击处理函数修改 isOpen,模态框在别处渲染,实际操作被移到 onConfirm 回调里。一个动作被分散到三个地方。
使用 SnappyModal.show() 时,处理函数打开模态框、等待回答,然后继续执行。局部变量保持可用,可以提前 return,一个 try/catch 就能覆盖整个流程。
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。它不知道要删除什么,因此应用中的所有确认流程都可以复用同一个组件。
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 | 与取消一样中止 |
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。请求失败时用另一个模态框显示错误,用户不会丢失上下文,函数也在同一处结束。
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,避免外部点击中途关闭模态框。
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。
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 时直接返回;如果可能有其他模态框打开,则使用局部的进行中标志。
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包裹。 - 重复点击不会打开第二个模态框。