🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

TanStack Query Guide 2026: React Data Fetching, Mutations and Caching

⏱️5 min read  ·  1,005 words

TanStack Query (formerly React Query) is the gold standard for server state management in React, Vue, Solid, and Angular applications in 2026. It eliminates the complexity of manual data fetching, caching, and synchronization, replacing hundreds of lines of useEffect code with simple, powerful hooks.

Why TanStack Query?

  • No boilerplate — replaces Redux + fetch + useEffect patterns
  • Automatic caching — data cached by key, refreshed when needed
  • Background refetching — stale data shown instantly, fresh data in background
  • Loading/error states — built-in, no manual state management
  • Optimistic updates — UI updates before server confirms
  • DevTools — inspect cache, refetch, invalidate from browser extension

Installation and Setup

npm install @tanstack/react-query @tanstack/react-query-devtools

# Optional: axios for HTTP
npm install axios

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,    // 5 minutes (data considered fresh)
      gcTime: 10 * 60 * 1000,      // 10 minutes (cache garbage collection)
      retry: 2,                     // retry failed requests twice
      refetchOnWindowFocus: true,   // refetch when tab becomes active
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

useQuery — Fetching Data

import { useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

interface User {
  id: number;
  name: string;
  email: string;
}

// Query function
async function fetchUser(id: number): Promise<User> {
  const { data } = await axios.get(`/api/users/${id}`);
  return data;
}

// Component using query
function UserProfile({ userId }: { userId: number }) {
  const {
    data: user,
    isLoading,
    isError,
    error,
    isFetching,        // true when refetching in background
    isStale,           // true when data is stale
    refetch,           // manually trigger refetch
  } = useQuery({
    queryKey: ['users', userId],    // unique cache key
    queryFn: () => fetchUser(userId),
    enabled: userId > 0,            // only fetch when userId is valid
    staleTime: 60_000,              // fresh for 1 minute
    select: (data) => data,         // transform data before returning
  });

  if (isLoading) return <Skeleton />;
  if (isError) return <ErrorBoundary error={error} />;

  return (
    <div>
      {isFetching && <small>Refreshing...</small>}
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  );
}

Parallel Queries

// Fetch multiple queries simultaneously
function Dashboard() {
  const [userQuery, postsQuery, statsQuery] = useQueries({
    queries: [
      { queryKey: ['user', 1], queryFn: () => fetchUser(1) },
      { queryKey: ['posts'], queryFn: fetchPosts },
      { queryKey: ['stats'], queryFn: fetchStats },
    ],
  });

  if (userQuery.isLoading || postsQuery.isLoading) return <Loading />;

  return (
    <div>
      <UserCard user={userQuery.data} />
      <PostList posts={postsQuery.data} />
      <StatsPanel stats={statsQuery.data} />
    </div>
  );
}

useMutation — Creating, Updating, Deleting

import { useMutation, useQueryClient } from '@tanstack/react-query';

async function createPost(post: { title: string; content: string }) {
  const { data } = await axios.post('/api/posts', post);
  return data;
}

function NewPostForm() {
  const queryClient = useQueryClient();

  const { mutate, isPending, isError, error } = useMutation({
    mutationFn: createPost,

    // Called when mutation succeeds
    onSuccess: (newPost) => {
      // Option 1: Invalidate cache (triggers refetch)
      queryClient.invalidateQueries({ queryKey: ['posts'] });

      // Option 2: Update cache directly (no refetch)
      queryClient.setQueryData(['posts'], (old: Post[]) => [...old, newPost]);

      // Show toast notification
      toast.success('Post created!');
    },

    onError: (error) => {
      toast.error(`Failed: ${error.message}`);
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const form = e.currentTarget;
    mutate({
      title: (form.title as HTMLInputElement).value,
      content: (form.content as HTMLTextAreaElement).value,
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="title" required placeholder="Title" />
      <textarea name="content" required placeholder="Content" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
      {isError && <p className="error">{error.message}</p>}
    </form>
  );
}

Optimistic Updates

const { mutate: likePost } = useMutation({
  mutationFn: (postId: number) => axios.post(`/api/posts/${postId}/like`),

  onMutate: async (postId) => {
    // Cancel any outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['posts', postId] });

    // Snapshot the previous value
    const previousPost = queryClient.getQueryData(['posts', postId]);

    // Optimistically update the UI
    queryClient.setQueryData(['posts', postId], (old: Post) => ({
      ...old,
      likes: old.likes + 1,
    }));

    // Return context for rollback
    return { previousPost };
  },

  // Roll back on error
  onError: (error, postId, context) => {
    queryClient.setQueryData(['posts', postId], context?.previousPost);
    toast.error('Failed to like post');
  },

  // Always refetch after mutation
  onSettled: (data, error, postId) => {
    queryClient.invalidateQueries({ queryKey: ['posts', postId] });
  },
});

Infinite Queries

import { useInfiniteQuery } from '@tanstack/react-query';

function InfinitePostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts', 'infinite'],
    queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
    initialPageParam: 1,
    getNextPageParam: (lastPage, pages) =>
      lastPage.hasMore ? pages.length + 1 : undefined,
  });

  return (
    <div>
      {data?.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.posts.map(post => <PostCard key={post.id} post={post} />)}
        </React.Fragment>
      ))}
      <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
        {isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'All loaded'}
      </button>
    </div>
  );
}

Custom Query Hooks

// hooks/useUsers.ts — reusable query hooks
export function useUsers(filters?: UserFilters) {
  return useQuery({
    queryKey: ['users', filters],
    queryFn: () => fetchUsers(filters),
    select: (data) => ({
      users: data.users,
      total: data.total,
    }),
  });
}

export function useUser(id: number) {
  const queryClient = useQueryClient();
  return useQuery({
    queryKey: ['users', id],
    queryFn: () => fetchUser(id),
    // Pre-populate from list query
    initialData: () => {
      const usersQuery = queryClient.getQueryData<UsersResponse>(['users']);
      return usersQuery?.users.find(u => u.id === id);
    },
    initialDataUpdatedAt: () =>
      queryClient.getQueryState(['users'])?.dataUpdatedAt,
  });
}

export function useUpdateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (user: Partial<User> & { id: number }) =>
      axios.patch(`/api/users/${user.id}`, user).then(r => r.data),
    onSuccess: (updated) => {
      queryClient.setQueryData(['users', updated.id], updated);
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

TanStack Query in 2026 eliminates 80% of the boilerplate in typical React data fetching code. Start with useQuery for reads and useMutation for writes. Use invalidateQueries for simple cache updates and setQueryData for optimistic updates. The learning investment pays dividends on every project.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা