Claude Code for TanStack Query Advanced: Optimistic Updates, Infinite Scroll, and Mutations — Claude Skills 360 Blog
Blog / Frontend / Claude Code for TanStack Query Advanced: Optimistic Updates, Infinite Scroll, and Mutations
Frontend

Claude Code for TanStack Query Advanced: Optimistic Updates, Infinite Scroll, and Mutations

Published: March 6, 2027
Read time: 8 min read
By: Claude Skills 360

TanStack Query manages server state in React applications — it handles caching, background refetching, loading states, and synchronization between tabs automatically. useQuery({ queryKey, queryFn }) fetches and caches data; useMutation({ mutationFn }) handles writes. Optimistic updates via onMutate update the cache instantly while the request is in-flight; onError rolls back if the mutation fails. useInfiniteQuery enables infinite scroll with fetchNextPage and getNextPageParam. queryClient.prefetchQuery populates the cache before navigation. Dependent queries with enabled: !!userId fire only when their dependencies resolve. Suspense mode integrates with React Suspense boundaries. persistQueryClient with createAsyncStoragePersister implements offline support. Claude Code generates TanStack Query hook compositions, mutation configurations with optimistic updates, infinite query patterns, and the query client setup for production React applications.

CLAUDE.md for TanStack Query

## TanStack Query Stack
- Version: @tanstack/react-query >= 5.0, @tanstack/react-query-devtools >= 5.0
- Keys: ["orders", customerId, { status, page }] — hierarchical, invalidate by prefix
- Mutation: useMutation({ mutationFn, onMutate, onError, onSettled }) — optimistic
- Infinite: useInfiniteQuery({ queryFn: ({pageParam}) => fetch, getNextPageParam })
- Prefetch: queryClient.prefetchQuery — populate cache before navigation
- Suspense: useSuspenseQuery / useSuspenseInfiniteQuery + <Suspense fallback>
- Stale: staleTime controls background refetch — 0 = always refetch in background
- Persist: persistQueryClient + createAsyncStoragePersister — offline cache

Query Client Setup

// lib/query-client.ts — shared QueryClient configuration
import { QueryClient } from "@tanstack/react-query"
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"
import { persistQueryClient } from "@tanstack/react-query-persist-client"

export function createQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,         // 1 minute before background refetch
        gcTime: 10 * 60 * 1000,       // 10 minutes in GC before eviction
        retry: (failureCount, error: any) => {
          // Don't retry on 4xx client errors
          if (error?.status >= 400 && error?.status < 500) return false
          return failureCount < 3
        },
        refetchOnWindowFocus: false,  // Don't refetch on tab switch
        refetchOnReconnect: "always",
      },
      mutations: {
        retry: 1,
      },
    },
  })
}

// Offline persistence with IndexedDB
export function setupPersistence(queryClient: QueryClient) {
  const persister = createAsyncStoragePersister({
    storage: window.indexedDB
      ? {
          getItem: async (key) => JSON.parse(localStorage.getItem(key) ?? "null"),
          setItem: async (key, value) => localStorage.setItem(key, JSON.stringify(value)),
          removeItem: async (key) => localStorage.removeItem(key),
        }
      : undefined,
    throttleTime: 5_000,
  })

  persistQueryClient({
    queryClient,
    persister,
    maxAge: 24 * 60 * 60 * 1000,  // 24 hours
    buster: process.env.NEXT_PUBLIC_BUILD_ID ?? "dev",
  })
}

Query Key Factory

// lib/query-keys.ts — centralized query key factory
const ordersKeys = {
  all: ["orders"] as const,
  lists: () => [...ordersKeys.all, "list"] as const,
  list: (filters: { customerId?: string; status?: string; page?: number }) =>
    [...ordersKeys.lists(), filters] as const,
  details: () => [...ordersKeys.all, "detail"] as const,
  detail: (id: string) => [...ordersKeys.details(), id] as const,
}

export const queryKeys = {
  orders: ordersKeys,
  products: {
    all: ["products"] as const,
    detail: (id: string) => ["products", "detail", id] as const,
    list: (filters: Record<string, unknown>) => ["products", "list", filters] as const,
  },
  user: {
    current: ["user", "me"] as const,
    profile: (id: string) => ["user", "profile", id] as const,
  },
}

// Usage: queryClient.invalidateQueries({ queryKey: queryKeys.orders.all })
// Invalidates ALL order queries (list and detail)

Optimistic Updates

// hooks/useOrders.ts — mutations with optimistic updates
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { queryKeys } from "@/lib/query-keys"
import { cancelOrder, updateOrderStatus } from "@/api/orders"
import type { Order } from "@/types"

export function useCancelOrder() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (orderId: string) => cancelOrder(orderId),

    // Optimistic update: immediately mark as cancelled in cache
    onMutate: async (orderId) => {
      // Cancel in-flight refetches that may overwrite our optimistic update
      await queryClient.cancelQueries({ queryKey: queryKeys.orders.all })

      // Snapshot the current value for rollback
      const previousOrders = queryClient.getQueryData<{ orders: Order[] }>(
        queryKeys.orders.lists()
      )

      // Optimistically update the list cache
      queryClient.setQueryData(
        queryKeys.orders.lists(),
        (old: { orders: Order[] } | undefined) => ({
          ...old,
          orders: old?.orders.map(o =>
            o.id === orderId ? { ...o, status: "cancelled" as const } : o
          ) ?? [],
        })
      )

      // Also update the detail cache if it exists
      queryClient.setQueryData(
        queryKeys.orders.detail(orderId),
        (old: Order | undefined) =>
          old ? { ...old, status: "cancelled" as const } : old
      )

      // Return context for rollback
      return { previousOrders, orderId }
    },

    // Rollback on error
    onError: (_err, orderId, context) => {
      if (context?.previousOrders) {
        queryClient.setQueryData(queryKeys.orders.lists(), context.previousOrders)
      }
      queryClient.setQueryData(queryKeys.orders.detail(orderId), (old: Order | undefined) =>
        old ? { ...old, status: "pending" as const } : old
      )
    },

    // Always refetch after settlement to sync with server
    onSettled: (_data, _err, orderId) => {
      queryClient.invalidateQueries({ queryKey: queryKeys.orders.all })
    },
  })
}

Infinite Scroll

// hooks/useInfiniteOrders.ts — infinite query
import { useInfiniteQuery } from "@tanstack/react-query"
import { queryKeys } from "@/lib/query-keys"
import { fetchOrders } from "@/api/orders"

export function useInfiniteOrders({
  customerId,
  status,
}: {
  customerId: string
  status?: string
}) {
  return useInfiniteQuery({
    queryKey: queryKeys.orders.list({ customerId, status }),
    queryFn: async ({ pageParam }) => {
      return fetchOrders({
        customerId,
        status,
        cursor: pageParam as string | undefined,
        limit: 20,
      })
    },
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
    staleTime: 30_000,
  })
}

// Usage in component
function OrdersList({ customerId }: { customerId: string }) {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
  } = useInfiniteOrders({ customerId })

  // Flatten pages into a single array
  const orders = data?.pages.flatMap(page => page.orders) ?? []

  // Intersection Observer for automatic load-more
  const loadMoreRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage()
        }
      },
      { threshold: 0.1 }
    )

    if (loadMoreRef.current) observer.observe(loadMoreRef.current)
    return () => observer.disconnect()
  }, [hasNextPage, isFetchingNextPage, fetchNextPage])

  return (
    <div>
      {orders.map(order => <OrderCard key={order.id} order={order} />)}
      <div ref={loadMoreRef} className="py-4 text-center">
        {isFetchingNextPage && <Spinner />}
        {!hasNextPage && orders.length > 0 && (
          <p className="text-muted-foreground">All orders loaded</p>
        )}
      </div>
    </div>
  )
}

Suspense Mode

// components/orders/OrderDetail.tsx — with Suspense
import { useSuspenseQuery } from "@tanstack/react-query"
import { Suspense } from "react"
import { ErrorBoundary } from "react-error-boundary"
import { queryKeys } from "@/lib/query-keys"
import { fetchOrder } from "@/api/orders"

function OrderDetailContent({ orderId }: { orderId: string }) {
  // Throws promise while loading (Suspense), throws error on failure (ErrorBoundary)
  const { data: order } = useSuspenseQuery({
    queryKey: queryKeys.orders.detail(orderId),
    queryFn: () => fetchOrder(orderId),
  })

  return (
    <div>
      <h1>Order #{order.id.slice(-8)}</h1>
      <p>Status: {order.status}</p>
      <p>Total: ${(order.totalCents / 100).toFixed(2)}</p>
    </div>
  )
}

export function OrderDetail({ orderId }: { orderId: string }) {
  return (
    <ErrorBoundary fallback={<p>Failed to load order</p>}>
      <Suspense fallback={<OrderDetailSkeleton />}>
        <OrderDetailContent orderId={orderId} />
      </Suspense>
    </ErrorBoundary>
  )
}

Prefetching

// app/orders/page.tsx — Next.js with prefetching
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query"
import { queryKeys } from "@/lib/query-keys"
import { fetchOrders } from "@/api/orders"
import { OrdersList } from "./OrdersList"

export default async function OrdersPage({ searchParams }: any) {
  const queryClient = new QueryClient()

  // Prefetch on server — populates the cache before hydration
  await queryClient.prefetchInfiniteQuery({
    queryKey: queryKeys.orders.list({ customerId: searchParams.customerId }),
    queryFn: ({ pageParam }) => fetchOrders({ cursor: pageParam as any, limit: 20 }),
    initialPageParam: undefined,
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <OrdersList customerId={searchParams.customerId} />
    </HydrationBoundary>
  )
}

For the SWR alternative from Vercel that provides similar data fetching with a simpler API and closer integration with Next.js — particularly for the Next.js unstable_serialize SSR pattern and useSWRInfinite for pagination, see the Next.js data fetching guide for comparison. For the TanStack Router integration that uses loader functions for data fetching at route level — combining routing and query prefetching with automatic suspense, see the TanStack Router guide for loader + TanStack Query patterns. The Claude Skills 360 bundle includes TanStack Query skill sets covering optimistic updates, infinite scroll, and suspense patterns. Start with the free tier to try query hook generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free