React Query: Production-Grade Patterns
Here are Production-Grade examples for React Query. These go beyond the basics. "Realistic" in a senior context means handling TypeScript, Optimistic Updates (updating UI before server responds), Race Conditions, and Data Transformation.
🔄 Unit 70: Advanced Mutations (The "Optimistic Update" Pattern)
⚡ Picture this: your user smashes that Like button and... nothing happens for 200ms. Awkward! 😬 Let's fix that with Optimistic Updates — the art of making your UI feel instant by updating before the server even responds. If things go south? We roll it right back like nothing happened! 🎭
🎯 The Flow:
1. User clicks "Like" ❤️
2. Immediately update the UI (Optimistic magic!)
3. Send request to server in the background 📡
4. If server fails, Rollback automatically 🔙
🔴 Impact: CRITICAL — Without optimistic updates, your app feels sluggish and users rage-click everything!
📋 In this section: onMutate snapshots • Optimistic UI updates • Automatic rollback on error • Cache invalidation with onSettled
// hooks/useToggleLike.tsimport { useMutation, useQueryClient } from '@tanstack/react-query';import { api } from '@/lib/api';import { Post } from '@/types';export const useToggleLike = () => {const queryClient = useQueryClient();return useMutation({mutationFn: (postId: string) => api.post(`/posts/${postId}/like`),// 1. ⚡ ON MUTATE: Run before the request is sentonMutate: async (postId) => {// Cancel any outgoing refetches (so they don't overwrite our optimistic update)await queryClient.cancelQueries({ queryKey: ['posts'] });// Snapshot the previous value (for rollback if needed)const previousPosts = queryClient.getQueryData<Post[]>(['posts']);// Optimistically update to the new valuequeryClient.setQueryData<Post[]>(['posts'], (old) => {return old?.map(post =>post.id === postId? { ...post, isLiked: !post.isLiked, likes: post.isLiked ? post.likes - 1 : post.likes + 1 }: post);});// Return a context object with the snapshotted valuereturn { previousPosts };},// 2. ❌ ON ERROR: If server fails, roll backonError: (err, newTodo, context) => {queryClient.setQueryData(['posts'], context?.previousPosts);toast.error("Could not update like status");},// 3. ✅ ON SETTLED: Always refetch after error or success to ensure synconSettled: () => {queryClient.invalidateQueries({ queryKey: ['posts'] });},});};// Component Usageconst LikeButton = ({ postId }: { postId: string }) => {const { mutate: toggleLike, isPending } = useToggleLike();return (<buttononClick={() => toggleLike(postId)}disabled={isPending}className="flex items-center gap-2"><HeartIcon filled={isLiked} /><span>{likes}</span></button>);};
🔍 Unit 71 & 73: Search Grid (Cancellation + KeepPreviousData)
🏎️ Ever typed fast in a search box and got stale results flashing? That's a race condition — and it's sneaky! 🐛 User types "Ap", then "Appl", and the slow "Ap" response arrives after "Appl" and overwrites it. Yikes!
🛡️ The fix: AbortSignal kills stale requests, and placeholderData keeps your table smooth — no blinky spinners between pages! ✨
🟠 Impact: HIGH — Race conditions silently corrupt your UI data and are notoriously hard to debug in production!
📋 In this section: AbortSignal cancellation • Race condition prevention • placeholderData for jitter-free pagination • staleTime caching
// hooks/useProductSearch.tsimport { useQuery } from '@tanstack/react-query';import { ProductResponse } from '@/types';// Standard Senior Pattern: Pass a simplified "filters" objectexport const useProductSearch = ({ page, search }: { page: number; search: string }) => {return useQuery({// 🔑 Key includes all dependencies. If 'search' changes, it refetches.queryKey: ['products', page, search],// ⚡ Query Function receives the 'signal' for cancellationqueryFn: async ({ signal }) => {const params = new URLSearchParams({page: page.toString(),q: search});// Pass the signal to fetch/axios.// If user types again, browser KILLS this request automatically.const res = await fetch(`/api/products?${params}`, { signal });if (!res.ok) throw new Error('Network error');return res.json() as Promise<ProductResponse>;},// 🛡️ UX: Keep showing Page 1 data while Page 2 is loading.// Prevents the "Loading Spinner Flash" between pages.placeholderData: (previousData) => previousData,// 🧠 Cache: Keep unused pages in memory for 5 mins so "Back" button is instantstaleTime: 1000 * 60 * 5,});};// Component Usageconst ProductTable = () => {const [page, setPage] = useState(1);const [search, setSearch] = useState("");const { data, isLoading, isFetching } = useProductSearch({ page, search });return (<div><inputvalue={search}onChange={(e) => setSearch(e.target.value)}placeholder="Search products..."/>{isLoading ? (<div>Loading...</div>) : (<><table>{data?.products.map(product => (<tr key={product.id}><td>{product.name}</td><td>{product.price}</td></tr>))}</table><div><button onClick={() => setPage(p => p - 1)} disabled={page === 1}>Previous</button><span>Page {page}</span><button onClick={() => setPage(p => p + 1)}>Next</button></div></>)}</div>);};
📜 Unit 72: Infinite Feed with Data Transformation
📱 Think Twitter, Instagram, TikTok — that endless scroll we all love (and can't stop using 😅). The API gives you pages of pages (arrays of arrays), but your component just wants one nice flat list. 🤷
✨ Enter the select trick — transform data at the query level so your components stay clean and blissfully unaware of pagination madness! 🧹
🔵 Impact: MEDIUM — Clean data transformation = happy components that don't need to know about pagination internals!
📋 In this section: useInfiniteQuery setup • select for data flattening • Intersection Observer auto-load • getNextPageParam cursors
// hooks/useInfiniteFeed.tsimport { useInfiniteQuery } from '@tanstack/react-query';export const useInfiniteFeed = () => {return useInfiniteQuery({queryKey: ['feed'],queryFn: async ({ pageParam = 1 }) => {const res = await fetch(`/api/feed?page=${pageParam}`);return res.json();},// Logic to find the next cursorgetNextPageParam: (lastPage, allPages) => {return lastPage.hasMore ? lastPage.nextPage : undefined;},// ✨ SENIOR TRICK: The 'select' option// Transform the data HERE, not in the component.// We flatten the "pages" array into a single "items" array.select: (data) => ({pages: data.pages,pageParams: data.pageParams,// Create a flat array for easy mapping in the UIitems: data.pages.flatMap((page) => page.results)}),});};// Component Usage (Clean UI)const Feed = () => {const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteFeed();// No need to map over pages in the UI anymore!// We can just map over 'data.items' because we transformed it above.return (<div>{data?.items.map(post => (<PostCard key={post.id} post={post} />))}{hasNextPage && (<buttononClick={() => fetchNextPage()}disabled={isFetchingNextPage}>{isFetchingNextPage ? 'Loading more...' : 'Load More'}</button>)}</div>);};// Alternative: Using Intersection Observer for auto-loadconst FeedWithAutoLoad = () => {const { data, fetchNextPage, hasNextPage } = useInfiniteFeed();const loadMoreRef = useRef<HTMLDivElement>(null);useEffect(() => {if (!loadMoreRef.current || !hasNextPage) return;const observer = new IntersectionObserver((entries) => {if (entries[0].isIntersecting) {fetchNextPage();}},{ threshold: 0.1 });observer.observe(loadMoreRef.current);return () => observer.disconnect();}, [hasNextPage, fetchNextPage]);return (<div>{data?.items.map(post => (<PostCard key={post.id} post={post} />))}<div ref={loadMoreRef} className="h-10" /></div>);};
🏆 Summary of Senior Practices Used
🟢 Impact: LOW — A quick recap to cement these patterns in your brain forever! 🧠
📋 In this section: Optimistic UI recap • AbortSignal recap • Selectors recap • Placeholder Data recap
⚡ 1. Optimistic UI: Used onMutate to make the app feel instant (Unit 70). The UI updates immediately, and if the server request fails, we automatically rollback to the previous state. 🔄
🛑 2. AbortSignal: Used for network efficiency and preventing race conditions (Unit 73). When a user types quickly, previous requests are automatically cancelled, ensuring only the latest data is displayed. 🏎️
🧹 3. Selectors: Used select to clean up data before it reaches the component (Unit 72). This keeps components focused on rendering and improves performance by transforming data at the query level. ✨
🎯 4. Placeholder Data: Used to create "Jitter-free" pagination (Unit 71). The previous page's data remains visible while the next page loads, creating a smooth user experience without loading spinners. 🧈