في TanStack Query تقدر تعمل Optimistic Updates باستخدام QueryClient. أول ما المستخدم يضغط Add to Cart، عدد المنتجات في الهيدر ومحتوى السلة يتحدثوا فورًا، من غير ما تستنى الـ Backend يرد.
الفكرة إننا نحدّث الكاش مؤقتًا، وبعدها:
- لو الـ API نجح، نعمل تحديث للداتا الحقيقية.
- لو الـ API فشل، نرجّع الكاش لحالته القديمة.
- لو فيه طلبات متزامنة، لازم ناخد بالنا إننا ما نمسحش تحديثات أحدث بالغلط.
التدفق
مفتاح الاستعلام والنوع
في FSD، خلّي مفاتيح السلة واستعلاماتها جوه entities/cart. كل الـ Features والـ Widgets تستخدم نفس التعريف.
import { queryOptions } from '@tanstack/react-query';import { getCart } from '../api/get-cart';export const cartKeys = { all: ['cart'] as const, current: () => [...cartKeys.all, 'current'] as const,};export const cartQueries = { current: () => queryOptions({ queryKey: cartKeys.current(), queryFn: getCart, }),};شكل السلة للتوضيح. استخدم شكل الـ Response الحقيقي اللي بيرجعه الـ Backend عندك.
export interface CartItem { productId: string; quantity: number; price: number;}export interface Cart { items: CartItem[];}التحديث المؤقت
import { useMutation, useQueryClient,} from '@tanstack/react-query';import { addCartItem } from '@/entities/cart';import { cartKeys } from '@/entities/cart/model/queries';import type { Cart } from '@/entities/cart/model/types';type AddCartItemInput = { productId: string; quantity: number;};export function useAddToCart() { const queryClient = useQueryClient(); const queryKey = cartKeys.current(); return useMutation({ mutationFn: (input: AddCartItemInput) => addCartItem(input), onMutate: async (input) => { await queryClient.cancelQueries({ queryKey, }); const previousCart = queryClient.getQueryData<Cart>(queryKey); if (previousCart) { queryClient.setQueryData<Cart>( queryKey, (oldCart) => { if (!oldCart) return oldCart; const existingItem = oldCart.items.find( (item) => item.productId === input.productId, ); if (existingItem) { return { ...oldCart, items: oldCart.items.map((item) => item.productId === input.productId ? { ...item, quantity: item.quantity + input.quantity, } : item, ), }; } return { ...oldCart, items: [ ...oldCart.items, { productId: input.productId, quantity: input.quantity, price: 0, }, ], }; }, ); } return { previousCart }; }, onError: (_error, _input, context) => { if (context?.previousCart) { queryClient.setQueryData( queryKey, context.previousCart, ); } }, onSettled: () => queryClient.invalidateQueries({ queryKey, }), });}onMutate يشتغل قبل الطلب. cancelQueries يمنع إعادة جلب شغالة من إنها تمسح التحديث المؤقت. previousCart للنسخ لو الطلب فشل. التحديث يحصل بس لو السلة موجودة في الكاش أصلًا. onError يرجّع النسخة القديمة. onSettled يعلّم البيانات إنها تتحدث من السيرفر، سواء نجح الطلب أو فشل.
الزرار
import { useAddToCart } from '../model/use-add-to-cart';export function AddToCartButton({ productId,}: { productId: string;}) { const mutation = useAddToCart(); return ( <button disabled={mutation.isPending} onClick={() => mutation.mutate({ productId, quantity: 1, }) } > {mutation.isPending ? 'Adding...' : 'Add to Cart'} </button> );}isPending يفضل true طول ما طلب الـ API شغال، حتى لو السلة اتحدثت في الواجهة. ده يمنع الضغط المتكرر. مش معناه إننا بنستنى السيرفر عشان نعرض التحديث.
الهيدر
import { useQuery } from '@tanstack/react-query';import { cartQueries } from '@/entities/cart';export function CartBadge() { const { data: cart } = useQuery( cartQueries.current(), ); const count = cart?.items.reduce( (total, item) => total + item.quantity, 0, ) ?? 0; return <span>{count}</span>;}طالما الهيدر وصفحة السلة بيستخدموا نفس QueryClient ونفس cartKeys.current()، تحديث الكاش بـ setQueryData يظهر للمكونات اللي بتراقب نفس الاستعلام.
أنهي دالة
| الدالة | وظيفتها |
|---|---|
setQueryData | تحديث الكاش فورًا بشكل مباشر |
getQueryData | قراءة القيمة الحالية من الكاش |
cancelQueries | إلغاء عمليات القراءة الجارية اللي ممكن تستبدل التحديث المؤقت |
invalidateQueries | تعليم البيانات إنها قديمة وطلب إعادة جلبها عند الحاجة |
التحديث الفوري مناسب لو تقدر تتوقع شكل النتيجة بدقة. لو العملية فيها مخزون وأسعار وخصومات وحسابات سيرفر، ممكن تكتفي بعدّاد مؤقت، أو تستنى رد السيرفر.
خلّصت الفصل لما
١) ليه cancelQueries قبل setQueryData؟
٢) الطلب فشل. الكاش يرجع إزاي؟
onError يحط previousCart تاني بـ setQueryData.٣) الهيدر ليه يتحدث قبل رد السيرفر؟
setQueryData عدّله.