1- import { useMutation , useQuery , useQueryClient } from "@tanstack/react-query" ;
1+ import { type QueryClient , useMutation , useQuery , useQueryClient } from "@tanstack/react-query" ;
22import {
33 addItemServerFn ,
44 getCartServerFn ,
55 removeItemServerFn ,
66 updateItemQuantityServerFn ,
77} from "./cart.actions" ;
8- import { EMPTY_CART , type CartState } from "./cart.types" ;
8+ import { type CartItem , EMPTY_CART , type CartState } from "./cart.types" ;
99
1010export const CART_QUERY_KEY = [ "cart" ] as const ;
1111
12+ interface OptimisticContext {
13+ prev : CartState ;
14+ }
15+
16+ /**
17+ * Applies new line items to a cart, re-deriving only the fields we can compute
18+ * client-side: `subtotal` (Σ price × qty) and `totalQuantity`. `total` is left
19+ * untouched on purpose — it may include discounts/shipping/tax we don't know
20+ * here — and is reconciled from the server cart in `onSuccess`.
21+ */
22+ function applyOptimisticItems ( cart : CartState , items : CartItem [ ] ) : CartState {
23+ return {
24+ ...cart ,
25+ items,
26+ totalQuantity : items . reduce ( ( n , i ) => n + i . quantity , 0 ) ,
27+ subtotal : {
28+ amount : items . reduce ( ( sum , i ) => sum + i . price . amount * i . quantity , 0 ) ,
29+ currencyCode : cart . subtotal . currencyCode ,
30+ } ,
31+ } ;
32+ }
33+
34+ /**
35+ * Shared optimistic-mutation plumbing: cancel in-flight cart fetches, snapshot
36+ * the current cart for rollback, and write the transformed items to the cache.
37+ */
38+ async function optimisticCartUpdate (
39+ qc : QueryClient ,
40+ transform : ( items : CartItem [ ] ) => CartItem [ ] ,
41+ ) : Promise < OptimisticContext > {
42+ await qc . cancelQueries ( { queryKey : CART_QUERY_KEY } ) ;
43+ const prev = qc . getQueryData < CartState > ( CART_QUERY_KEY ) ?? EMPTY_CART ;
44+ qc . setQueryData ( CART_QUERY_KEY , applyOptimisticItems ( prev , transform ( prev . items ) ) ) ;
45+ return { prev } ;
46+ }
47+
48+ function rollbackCart ( qc : QueryClient , ctx : OptimisticContext | undefined ) {
49+ if ( ctx ?. prev ) qc . setQueryData ( CART_QUERY_KEY , ctx . prev ) ;
50+ }
51+
1252export function useCart ( ) {
1353 const query = useQuery ( {
1454 queryKey : CART_QUERY_KEY ,
@@ -27,8 +67,18 @@ export function useCart() {
2767export function useAddToCart ( ) {
2868 const qc = useQueryClient ( ) ;
2969 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" ] ,
3076 mutationFn : ( input : { merchandiseId : string ; quantity ?: number } ) =>
3177 addItemServerFn ( { data : input } ) ,
78+ // NOTE(DECO-5278): optimistic add is deferred — building an optimistic
79+ // line needs a product snapshot (title/image/price). It should come from a
80+ // neutral `productToCartItem` mapping over commerce types, tracked as a
81+ // remaining sub-item. Add already shows button feedback while in flight.
3282 onSuccess : ( cart : CartState ) => {
3383 qc . setQueryData ( CART_QUERY_KEY , cart ) ;
3484 } ,
@@ -38,8 +88,15 @@ export function useAddToCart() {
3888export function useUpdateCartItem ( ) {
3989 const qc = useQueryClient ( ) ;
4090 return useMutation ( {
91+ scope : { id : "cart" } ,
92+ mutationKey : [ "cart" , "update" ] ,
4193 mutationFn : ( input : { lineId : string ; quantity : number } ) =>
4294 updateItemQuantityServerFn ( { data : input } ) ,
95+ onMutate : ( { lineId, quantity } ) =>
96+ optimisticCartUpdate ( qc , ( items ) =>
97+ items . map ( ( i ) => ( i . lineId === lineId ? { ...i , quantity : Math . max ( 1 , quantity ) } : i ) ) ,
98+ ) ,
99+ onError : ( _err , _input , ctx ) => rollbackCart ( qc , ctx ) ,
43100 onSuccess : ( cart : CartState ) => {
44101 qc . setQueryData ( CART_QUERY_KEY , cart ) ;
45102 } ,
@@ -49,8 +106,12 @@ export function useUpdateCartItem() {
49106export function useRemoveCartItem ( ) {
50107 const qc = useQueryClient ( ) ;
51108 return useMutation ( {
52- mutationFn : ( input : { lineId : string } ) =>
53- removeItemServerFn ( { data : input } ) ,
109+ scope : { id : "cart" } ,
110+ mutationKey : [ "cart" , "remove" ] ,
111+ mutationFn : ( input : { lineId : string } ) => removeItemServerFn ( { data : input } ) ,
112+ onMutate : ( { lineId } ) =>
113+ optimisticCartUpdate ( qc , ( items ) => items . filter ( ( i ) => i . lineId !== lineId ) ) ,
114+ onError : ( _err , _input , ctx ) => rollbackCart ( qc , ctx ) ,
54115 onSuccess : ( cart : CartState ) => {
55116 qc . setQueryData ( CART_QUERY_KEY , cart ) ;
56117 } ,
0 commit comments