Skip to content

Commit 90e6955

Browse files
os-zhuangclaude
andauthored
feat(ux): optimistic swipe-delete on list (#138)
Deleting a row from the list now removes it instantly on confirm instead of waiting for the server round-trip + refetch. A pendingDeletes set hides the row optimistically; a failed delete restores it and toasts the error, and the set auto-prunes once a refetch reflects the deletion so it can't grow across a long session. The record count reflects the optimistic state too. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 294a83d commit 90e6955

1 file changed

Lines changed: 32 additions & 2 deletions

File tree

‎app/(app)/[appName]/[objectName]/index.tsx‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Plus } from "lucide-react-native";
55
import { useClient, useQuery, useView } from "@objectstack/client-react";
66
import { useTranslation } from "react-i18next";
77
import { tCount } from "~/lib/i18n";
8-
import { useCallback, useState } from "react";
8+
import { useCallback, useEffect, useState } from "react";
99
import { ListViewRenderer } from "~/components/renderers";
1010
import type { ListViewMeta } from "~/components/renderers";
1111
import { ScreenHeader } from "~/components/common/ScreenHeader";
@@ -44,7 +44,29 @@ export default function ObjectListScreen() {
4444
objectName?.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) ??
4545
"Objects";
4646

47-
const records = data?.records ?? [];
47+
// Rows deleted optimistically: hidden from the list the instant you confirm,
48+
// before the server round-trip lands. Restored on failure (see below).
49+
const [pendingDeletes, setPendingDeletes] = useState<Set<string>>(new Set());
50+
const allRecords = data?.records ?? [];
51+
const records = allRecords.filter(
52+
(r: Record<string, unknown>) => !pendingDeletes.has(String(r.id ?? r._id)),
53+
);
54+
55+
// Prune the pending set once a refetch reflects the deletion (the id is gone
56+
// from server data), so it can't grow unbounded across a long session.
57+
useEffect(() => {
58+
if (pendingDeletes.size === 0) return;
59+
const present = new Set(
60+
(data?.records ?? []).map((r: Record<string, unknown>) =>
61+
String(r.id ?? r._id),
62+
),
63+
);
64+
setPendingDeletes((prev) => {
65+
const next = new Set([...prev].filter((id) => present.has(id)));
66+
return next.size === prev.size ? prev : next;
67+
});
68+
}, [data, pendingDeletes.size]);
69+
4870
const listView: ListViewMeta | undefined = viewData ?? undefined;
4971

5072
const handleSwipeEdit = useCallback(
@@ -66,10 +88,18 @@ export default function ObjectListScreen() {
6688
destructive: true,
6789
});
6890
if (!ok) return;
91+
// Optimistic: drop the row immediately, then persist.
92+
setPendingDeletes((prev) => new Set(prev).add(id));
6993
try {
7094
await client.data.delete(objectName!, id);
7195
refetch();
7296
} catch {
97+
// Restore the row and surface the failure.
98+
setPendingDeletes((prev) => {
99+
const next = new Set(prev);
100+
next.delete(id);
101+
return next;
102+
});
73103
toastError(t("records.deleteFailed"));
74104
}
75105
},

0 commit comments

Comments
 (0)