Skip to content

Commit db64628

Browse files
committed
fix(web): debounce cart edits so typing isn't reverted by in-flight saves
Title/bio/cover saved on every keystroke and then overwrote local state with the server echo — an earlier keystroke's response would land and revert the field while typing. Now: local state is authoritative, edits are batched and PATCHed ~600ms after the last change (and flushed on leave), with no clobber of the input.
1 parent af9ac3b commit db64628

1 file changed

Lines changed: 33 additions & 11 deletions

File tree

web/app/dashboard/carts/[id]/editor.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useTransition } from "react";
3+
import { useState, useRef, useEffect } from "react";
44
import Image from "next/image";
55
import Link from "next/link";
66
import { useRouter } from "next/navigation";
@@ -54,20 +54,42 @@ export function CartEditor({ initialCart }: { initialCart: Cart }) {
5454
const router = useRouter();
5555
const [cart, setCart] = useState<Cart>(initialCart);
5656
const [deleting, setDeleting] = useState(false);
57-
const [, startTransition] = useTransition();
5857

59-
const patch = async (changes: Partial<Cart>) => {
58+
// Debounced save: local state is the source of truth while editing. We
59+
// accumulate changed fields and PATCH them ~600ms after the last keystroke,
60+
// and we DON'T overwrite local state with the server echo — otherwise an
61+
// in-flight save for an earlier keystroke lands and reverts what you're
62+
// typing (text "getting replaced").
63+
const pending = useRef<Partial<Cart>>({});
64+
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
65+
66+
const flushSave = async () => {
67+
const changes = pending.current;
68+
pending.current = {};
69+
if (Object.keys(changes).length === 0) return;
70+
try {
71+
await updateCart(initialCart.id, changes);
72+
} catch {
73+
toast.error("Couldn't save changes. Please try again.");
74+
}
75+
};
76+
77+
const patch = (changes: Partial<Cart>) => {
6078
setCart((c) => ({ ...c, ...changes }));
61-
startTransition(async () => {
62-
try {
63-
const updated = await updateCart(cart.id, changes);
64-
setCart(updated);
65-
} catch {
66-
toast.error("Couldn't save changes. Please try again.");
67-
}
68-
});
79+
pending.current = { ...pending.current, ...changes };
80+
if (saveTimer.current) clearTimeout(saveTimer.current);
81+
saveTimer.current = setTimeout(flushSave, 600);
6982
};
7083

84+
// Flush any pending edit when leaving the editor.
85+
useEffect(() => {
86+
return () => {
87+
if (saveTimer.current) clearTimeout(saveTimer.current);
88+
void flushSave();
89+
};
90+
// eslint-disable-next-line react-hooks/exhaustive-deps
91+
}, []);
92+
7193
const addProduct = async (draft: Omit<Product, "id">) => {
7294
try {
7395
const updated = await addProductToCart(cart.id, draft);

0 commit comments

Comments
 (0)