Back to Blog

React Query Patterns in 2026: Query Invalidation, Optimistic Updates, and Infinite Scroll

Master TanStack Query v5 patterns: query key factories, cache invalidation strategies, optimistic mutations, infinite scroll with useInfiniteQuery, prefetching, and server-state architecture.

Viprasol Tech Team
August 31, 2026
13 min read

React Query Patterns in 2026: Query Invalidation, Optimistic Updates, and Infinite Scroll

TanStack Query (formerly React Query) has become the standard for server-state management in React applications. The common usage pattern โ€” slap useQuery on every data fetch โ€” works fine for simple cases but breaks down at scale. The patterns that distinguish production codebases from demos: query key factories that make cache invalidation predictable, optimistic mutations that feel instantaneous, and infinite queries that don't re-fetch the entire list on mutation.

This post covers the patterns we use in production applications.


Query Key Factories

Query keys are the foundation of TanStack Query's cache. Ad-hoc keys like ['users', userId] scattered across components make cache invalidation unpredictable. Query key factories centralize key construction:

// src/lib/query-keys.ts
// Centralized query key factory โ€” all cache keys defined in one place

export const queryKeys = {
  // Users
  users: {
    all: () => ['users'] as const,
    lists: () => [...queryKeys.users.all(), 'list'] as const,
    list: (filters: UserFilters) => [...queryKeys.users.lists(), filters] as const,
    details: () => [...queryKeys.users.all(), 'detail'] as const,
    detail: (id: string) => [...queryKeys.users.details(), id] as const,
  },

  // Posts
  posts: {
    all: () => ['posts'] as const,
    lists: () => [...queryKeys.posts.all(), 'list'] as const,
    list: (filters: PostFilters) => [...queryKeys.posts.lists(), filters] as const,
    detail: (id: string) => [...queryKeys.posts.all(), 'detail', id] as const,
    comments: (postId: string) => [...queryKeys.posts.detail(postId), 'comments'] as const,
  },

  // Orders
  orders: {
    all: () => ['orders'] as const,
    byUser: (userId: string) => [...queryKeys.orders.all(), 'user', userId] as const,
    detail: (id: string) => [...queryKeys.orders.all(), 'detail', id] as const,
  },
} as const;

// Usage:
// queryClient.invalidateQueries({ queryKey: queryKeys.posts.lists() })
// โ†’ invalidates ALL post lists (any filter combination)
// โ†’ does NOT invalidate post details

Query Hooks with Error Boundaries

// src/hooks/queries/useUser.ts
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { api } from '@/lib/api';

// Standard query hook (handles loading/error state inline)
export function useUser(userId: string) {
  return useQuery({
    queryKey: queryKeys.users.detail(userId),
    queryFn: () => api.users.get(userId),
    enabled: !!userId,        // Don't fetch if userId is empty
    staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
    gcTime: 10 * 60 * 1000,   // Keep in cache for 10 minutes (formerly cacheTime)
    retry: (failureCount, error) => {
      // Don't retry 404s โ€” user doesn't exist
      if ((error as any)?.status === 404) return false;
      return failureCount < 3;
    },
  });
}

// Suspense variant (throws to nearest Suspense boundary)
export function useSuspenseUser(userId: string) {
  return useSuspenseQuery({
    queryKey: queryKeys.users.detail(userId),
    queryFn: () => api.users.get(userId),
    staleTime: 5 * 60 * 1000,
  });
}

// Usage with Suspense + Error Boundary:
// <ErrorBoundary fallback={<Error />}>
//   <Suspense fallback={<Skeleton />}>
//     <UserProfile userId={id} />
//   </Suspense>
// </ErrorBoundary>
//
// function UserProfile({ userId }) {
//   const { data: user } = useSuspenseUser(userId); // never undefined
//   return <div>{user.name}</div>;
// }

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Cache Invalidation Strategies

// src/hooks/mutations/useUpdateUser.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { api } from '@/lib/api';

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ userId, data }: { userId: string; data: UpdateUserInput }) =>
      api.users.update(userId, data),

    onSuccess: (updatedUser) => {
      // Strategy 1: Update cache directly (no refetch needed)
      queryClient.setQueryData(
        queryKeys.users.detail(updatedUser.id),
        updatedUser,
      );

      // Strategy 2: Invalidate list queries (they'll refetch on next access)
      queryClient.invalidateQueries({
        queryKey: queryKeys.users.lists(),
      });

      // Don't invalidate the detail โ€” we already updated it above
    },

    onError: (error) => {
      console.error('Failed to update user:', error);
    },
  });
}

// More targeted invalidation patterns:
export function useDeletePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) => api.posts.delete(postId),

    onSuccess: (_, postId) => {
      // Remove from cache entirely (rather than refetching a deleted resource)
      queryClient.removeQueries({
        queryKey: queryKeys.posts.detail(postId),
      });

      // Invalidate all post lists โ€” the deleted post needs to disappear
      queryClient.invalidateQueries({
        queryKey: queryKeys.posts.lists(),
      });
    },
  });
}

Optimistic Updates

Optimistic updates make mutations feel instantaneous by updating the cache before the server responds:

// src/hooks/mutations/useToggleLike.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';

interface Post {
  id: string;
  title: string;
  likeCount: number;
  isLikedByCurrentUser: boolean;
}

export function useToggleLike(postId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (isLiked: boolean) =>
      isLiked ? api.posts.unlike(postId) : api.posts.like(postId),

    onMutate: async (currentlyLiked: boolean) => {
      // 1. Cancel in-flight queries to prevent race conditions
      await queryClient.cancelQueries({
        queryKey: queryKeys.posts.detail(postId),
      });

      // 2. Snapshot current value for rollback
      const previous = queryClient.getQueryData<Post>(queryKeys.posts.detail(postId));

      // 3. Optimistically update the cache
      queryClient.setQueryData<Post>(
        queryKeys.posts.detail(postId),
        (old) => old ? {
          ...old,
          isLikedByCurrentUser: !currentlyLiked,
          likeCount: old.likeCount + (currentlyLiked ? -1 : 1),
        } : old,
      );

      // 4. Return snapshot for rollback in onError
      return { previous };
    },

    onError: (_err, _variables, context) => {
      // Roll back to previous value if mutation failed
      if (context?.previous) {
        queryClient.setQueryData(
          queryKeys.posts.detail(postId),
          context.previous,
        );
      }
    },

    onSettled: () => {
      // Always refetch to ensure server state is authoritative
      queryClient.invalidateQueries({
        queryKey: queryKeys.posts.detail(postId),
      });
    },
  });
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Infinite Scroll with useInfiniteQuery

// src/hooks/queries/useInfinitePosts.ts
import { useInfiniteQuery } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';

interface PostsPage {
  posts: Post[];
  nextCursor: string | null;
  total: number;
}

export function useInfinitePosts(filters: PostFilters = {}) {
  return useInfiniteQuery({
    queryKey: queryKeys.posts.list(filters),
    queryFn: ({ pageParam }) =>
      api.posts.list({ ...filters, cursor: pageParam, limit: 20 }),
    initialPageParam: null as string | null,
    getNextPageParam: (lastPage: PostsPage) => lastPage.nextCursor,
    staleTime: 2 * 60 * 1000,
  });
}
// src/components/PostFeed.tsx
import { useInfinitePosts } from '@/hooks/queries/useInfinitePosts';
import { useIntersectionObserver } from '@/hooks/useIntersectionObserver';

export function PostFeed() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfinitePosts();

  // Auto-fetch when sentinel element enters viewport
  const sentinelRef = useIntersectionObserver(
    (isIntersecting) => {
      if (isIntersecting && hasNextPage && !isFetchingNextPage) {
        fetchNextPage();
      }
    },
    { threshold: 0.1 },
  );

  if (status === 'pending') return <PostSkeleton count={5} />;
  if (status === 'error') return <ErrorState />;

  const posts = data.pages.flatMap((page) => page.posts);

  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}

      {/* Sentinel: invisible element at the bottom */}
      <div ref={sentinelRef} className="h-4" />

      {isFetchingNextPage && <PostSkeleton count={3} />}

      {!hasNextPage && posts.length > 0 && (
        <p className="text-center text-gray-500 py-8">
          You've reached the end
        </p>
      )}
    </div>
  );
}

// Intersection Observer hook
function useIntersectionObserver(
  callback: (isIntersecting: boolean) => void,
  options?: IntersectionObserverInit,
) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const observer = new IntersectionObserver(
      ([entry]) => callback(entry.isIntersecting),
      options,
    );

    observer.observe(element);
    return () => observer.disconnect();
  }, [callback, options]);

  return ref;
}

Prefetching for Instant Navigation

// src/components/PostList.tsx โ€” Prefetch on hover for instant navigation

import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';

export function PostListItem({ post }: { post: Post }) {
  const queryClient = useQueryClient();

  // Prefetch post detail on hover โ€” ready before user clicks
  const handleMouseEnter = useCallback(() => {
    queryClient.prefetchQuery({
      queryKey: queryKeys.posts.detail(post.id),
      queryFn: () => api.posts.get(post.id),
      staleTime: 5 * 60 * 1000,
    });
  }, [queryClient, post.id]);

  return (
    <Link
      href={`/posts/${post.id}`}
      onMouseEnter={handleMouseEnter}  // Prefetch on hover
      onFocus={handleMouseEnter}       // Accessibility: prefetch on focus too
    >
      {post.title}
    </Link>
  );
}

Global Query Client Configuration

// src/providers/QueryProvider.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        // Don't refetch on window focus in dev (annoying during development)
        refetchOnWindowFocus: process.env.NODE_ENV === 'production',
        // Don't refetch on reconnect for short disconnects
        refetchOnReconnect: 'always',
        // Global stale time: 1 minute (override per query as needed)
        staleTime: 60 * 1000,
        // Keep in cache for 5 minutes after component unmounts
        gcTime: 5 * 60 * 1000,
        // Retry with exponential backoff, max 3 times
        retry: 3,
        retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
      },
      mutations: {
        // Don't retry mutations by default (idempotency concern)
        retry: false,
        onError: (error) => {
          // Global error handler โ€” show toast, log to Sentry
          console.error('Mutation error:', error);
        },
      },
    },
  });
}

export function QueryProvider({ children }: { children: React.ReactNode }) {
  // Use state to ensure each render gets same QueryClient instance
  const [queryClient] = useState(() => makeQueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      {process.env.NODE_ENV === 'development' && (
        <ReactQueryDevtools initialIsOpen={false} />
      )}
    </QueryClientProvider>
  );
}

Working With Viprasol

We architect React applications with production-grade server-state management โ€” from query key design through cache invalidation, optimistic mutations, and prefetching strategies.

What we deliver:

  • Query key factory design for predictable cache invalidation
  • Mutation patterns with optimistic updates and rollback
  • Infinite scroll implementation with cursor-based pagination
  • Query client configuration for production workloads
  • Migration from Redux/SWR to TanStack Query

โ†’ Discuss your React architecture โ†’ Web development services


Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.