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. ๐ง