Skip to content

Commit c418957

Browse files
JonasJesus42claude
andcommitted
feat(cart): serialize mutations via scope + global busy indicator (DECO-5291)
Stacked on #16 (DECO-5278). Completes the cart-optimistic story: - scope: { id: "cart" } on all cart mutations → TanStack runs them serially, fixing the out-of-order race (server sets absolute quantities). - mutationKey ["cart", …] so useMutationState can surface a global busy state. - Bag.tsx: spinner while any cart mutation is in flight (no prop drilling). - Minicart: drop the QuantityStepper disabled={pending} freeze — now safe with optimistic + serialization (was deferred from #16 to here). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1bee7b8 commit c418957

3 files changed

Lines changed: 25 additions & 4 deletions

File tree

src/components/header/Bag.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import { useMutationState } from "@tanstack/react-query";
12
import { MINICART_DRAWER_ID } from "../../constants";
23
import Icon from "../ui/Icon";
34
import { useCart } from "../../platform/cart";
45

56
export default function Bag() {
67
const { cart } = useCart();
78
const count = cart.items.length;
9+
// Global "cart busy" indicator: any in-flight cart mutation (add/update/
10+
// remove) from anywhere in the tree, read via useMutationState by the
11+
// ["cart", …] mutationKey — no prop drilling.
12+
const busy = useMutationState({
13+
filters: { mutationKey: ["cart"], status: "pending" },
14+
}).length > 0;
815
return (
916
<label
1017
className="indicator"
@@ -17,7 +24,9 @@ export default function Bag() {
1724
</span>
1825
)}
1926
<span className="btn btn-square btn-sm btn-ghost no-animation">
20-
<Icon id="shopping_bag" />
27+
{busy
28+
? <span className="loading loading-spinner loading-xs" />
29+
: <Icon id="shopping_bag" />}
2130
</span>
2231
</label>
2332
);

src/components/minicart/Minicart.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import {
1313

1414
function QuantityStepper({ item }: { item: CartItem }) {
1515
const update = useUpdateCartItem();
16-
const pending = update.isPending && update.variables?.lineId === item.lineId;
1716
const set = (quantity: number) =>
1817
update.mutate({ lineId: item.lineId, quantity: Math.max(1, quantity) });
18+
// No `pending` freeze: the quantity updates optimistically on click and the
19+
// "cart" mutation scope serializes the requests, so rapid clicks stay
20+
// consistent and the buttons remain interactive. Only the lower bound is
21+
// disabled.
1922
return (
2023
<div className="join border border-base-200 rounded">
2124
<button
2225
type="button"
2326
className="join-item btn btn-ghost btn-sm no-animation"
2427
aria-label="Decrease quantity"
25-
disabled={pending || item.quantity <= 1}
28+
disabled={item.quantity <= 1}
2629
onClick={() => set(item.quantity - 1)}
2730
>
2831
-
@@ -34,7 +37,6 @@ function QuantityStepper({ item }: { item: CartItem }) {
3437
type="button"
3538
className="join-item btn btn-ghost btn-sm no-animation"
3639
aria-label="Increase quantity"
37-
disabled={pending}
3840
onClick={() => set(item.quantity + 1)}
3941
>
4042
+

src/platform/cart/cart.hooks.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export function useCart() {
6767
export function useAddToCart() {
6868
const qc = useQueryClient();
6969
return useMutation({
70+
// Serialize all cart mutations (same scope id) so rapid actions run in
71+
// order — the server sets absolute quantities, so out-of-order responses
72+
// would otherwise clobber the cache. mutationKey lets useMutationState
73+
// surface a global "cart busy" indicator.
74+
scope: { id: "cart" },
75+
mutationKey: ["cart", "add"],
7076
mutationFn: (input: { merchandiseId: string; quantity?: number }) =>
7177
addItemServerFn({ data: input }),
7278
// NOTE(DECO-5278): optimistic add is deferred — building an optimistic
@@ -82,6 +88,8 @@ export function useAddToCart() {
8288
export function useUpdateCartItem() {
8389
const qc = useQueryClient();
8490
return useMutation({
91+
scope: { id: "cart" },
92+
mutationKey: ["cart", "update"],
8593
mutationFn: (input: { lineId: string; quantity: number }) =>
8694
updateItemQuantityServerFn({ data: input }),
8795
onMutate: ({ lineId, quantity }) =>
@@ -98,6 +106,8 @@ export function useUpdateCartItem() {
98106
export function useRemoveCartItem() {
99107
const qc = useQueryClient();
100108
return useMutation({
109+
scope: { id: "cart" },
110+
mutationKey: ["cart", "remove"],
101111
mutationFn: (input: { lineId: string }) => removeItemServerFn({ data: input }),
102112
onMutate: ({ lineId }) =>
103113
optimisticCartUpdate(qc, (items) => items.filter((i) => i.lineId !== lineId)),

0 commit comments

Comments
 (0)