Fix: TanStack Query Not Working — Data Not Fetching, Cache Not Updating, or Mutation Not Triggering Re-render
Quick Answer
How to fix TanStack Query (React Query v5) issues — query keys, stale time, enabled flag, mutation callbacks, optimistic updates, QueryClient setup, and SSR with prefetchQuery.
The Problem
A query never fires — the component stays in loading state forever:
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
// isLoading is true indefinitely — queryFn never runsOr a mutation succeeds but the list doesn’t update:
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
// User created, but the list still shows old data
},
});Or data goes stale immediately and refetches on every focus:
const { data } = useQuery({
queryKey: ['config'],
queryFn: fetchConfig,
// Refetches every time the window regains focus — causing flicker
});Or nested query keys aren’t matching for cache invalidation:
// After creating a user, invalidating the cache doesn't refresh the list
queryClient.invalidateQueries({ queryKey: ['users'] });
// But the query was defined with queryKey: ['users', { role: 'admin' }]Why This Happens
TanStack Query (React Query v5) manages server state with specific caching rules:
queryFnis required — ifqueryFnis undefined or theenabledflag isfalse, the query never fires. No error is thrown — it just stays in the pending state.QueryClientmust be provided at the root — every component that callsuseQuerymust have aQueryClientProviderancestor. Missing the provider throws a runtime error.- Query keys must be serializable and stable — query keys are compared by deep equality. Objects or arrays that change reference on every render cause infinite refetch loops.
- Mutations don’t automatically update the cache — after a mutation, you must either invalidate affected queries or manually update the cache with
setQueryData. TanStack Query doesn’t know which queries a mutation affects. - Stale time defaults to 0 — by default, all data is considered stale immediately. Any window focus, reconnection, or component remount triggers a background refetch.
Fix 1: Set Up QueryClient Correctly
// src/main.tsx (or app entry point)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes — data stays fresh
gcTime: 1000 * 60 * 10, // 10 minutes — keep in cache after unmount
retry: 2, // Retry failed queries twice
refetchOnWindowFocus: false, // Disable refetch on tab focus (optional)
refetchOnReconnect: true,
},
mutations: {
retry: 0, // Don't retry failed mutations by default
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
{/* DevTools — shows in development only */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Verify setup with React Query DevTools — the DevTools panel shows all active queries, their state, data, and when they last fetched. Install with npm install @tanstack/react-query-devtools.
Fix 2: Write Queries Correctly
import { useQuery, keepPreviousData } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
// Basic query
function UserList() {
const {
data,
isLoading, // True on first load (no cached data)
isFetching, // True whenever a request is in flight (incl. background)
isError,
error,
refetch,
} = useQuery({
queryKey: ['users'],
queryFn: async (): Promise<User[]> => {
const res = await fetch('/api/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
staleTime: 1000 * 60 * 5, // 5 min — override global default
});
if (isLoading) return <Spinner />;
if (isError) return <Error message={error.message} />;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// Query with parameters — include ALL variables in the key
function UserProfile({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId], // Key includes userId
queryFn: () => fetchUser(userId),
enabled: !!userId, // Only fetch when userId is defined
});
return <div>{user?.name}</div>;
}
// Paginated query
function PaginatedUsers() {
const [page, setPage] = useState(1);
const { data, isPlaceholderData } = useQuery({
queryKey: ['users', { page }], // Page is part of the key
queryFn: () => fetchUsers({ page }),
placeholderData: keepPreviousData, // Show old data while fetching new page
});
return (
<>
{data?.users.map(u => <li key={u.id}>{u.name}</li>)}
<button
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data?.hasMore}
>
Next
</button>
</>
);
}Query key best practices:
// Consistent key structure — use arrays
const queryKeys = {
all: ['users'] as const,
lists: () => [...queryKeys.all, 'list'] as const,
list: (filters: Filters) => [...queryKeys.lists(), filters] as const,
details: () => [...queryKeys.all, 'detail'] as const,
detail: (id: number) => [...queryKeys.details(), id] as const,
};
// Usage
useQuery({ queryKey: queryKeys.detail(userId), queryFn: ... });
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: queryKeys.all });
// Invalidate only list queries
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });Fix 3: Fix Mutations and Cache Updates
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUserForm() {
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: async (newUser: CreateUserInput) => {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<User>;
},
// Option 1: Invalidate the cache — triggers a refetch
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
// Option 2: Manually update the cache — no extra request
onSuccess: (newUser) => {
queryClient.setQueryData<User[]>(['users'], (old) => {
return old ? [...old, newUser] : [newUser];
});
},
onError: (error) => {
console.error('Failed to create user:', error);
},
});
return (
<form onSubmit={(e) => {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
createMutation.mutate({
name: data.get('name') as string,
email: data.get('email') as string,
});
}}>
<input name="name" />
<input name="email" />
<button disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating...' : 'Create User'}
</button>
{createMutation.isError && (
<p>Error: {createMutation.error.message}</p>
)}
</form>
);
}Optimistic updates — show changes immediately:
const updateMutation = useMutation({
mutationFn: (update: { id: number; name: string }) =>
fetch(`/api/users/${update.id}`, {
method: 'PATCH',
body: JSON.stringify({ name: update.name }),
}).then(r => r.json()),
onMutate: async (update) => {
// Cancel any in-flight refetches
await queryClient.cancelQueries({ queryKey: ['users'] });
// Snapshot the current value
const previous = queryClient.getQueryData<User[]>(['users']);
// Optimistically update the cache
queryClient.setQueryData<User[]>(['users'], (old) =>
old?.map(u => u.id === update.id ? { ...u, name: update.name } : u) ?? []
);
// Return snapshot for rollback
return { previous };
},
onError: (err, update, context) => {
// Rollback on error
if (context?.previous) {
queryClient.setQueryData(['users'], context.previous);
}
},
onSettled: () => {
// Always refetch after mutation to sync with server
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});Fix 4: Fix Infinite Queries for Pagination
import { useInfiniteQuery } from '@tanstack/react-query';
interface UsersPage {
users: User[];
nextCursor: string | null;
}
function InfiniteUserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['users', 'infinite'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/users?cursor=${pageParam ?? ''}`);
return res.json() as Promise<UsersPage>;
},
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
// getNextPageParam returns undefined to stop fetching
});
// Flatten pages into a single array
const users = data?.pages.flatMap(page => page.users) ?? [];
return (
<>
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No More'}
</button>
</>
);
}Fix 5: Use SSR and Prefetching
For Next.js and other SSR frameworks, prefetch queries on the server:
// Next.js App Router — using TanStack Query with server components
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';
import { cache } from 'react';
// Create a new QueryClient per request (cache() ensures one per request)
export const getQueryClient = cache(() => new QueryClient());// app/users/page.tsx (Server Component)
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient } from '@/lib/query-client';
import UserList from './UserList';
export default async function UsersPage() {
const queryClient = getQueryClient();
// Prefetch on the server
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return (
// Dehydrate the cache and send to client
<HydrationBoundary state={dehydrate(queryClient)}>
<UserList />
</HydrationBoundary>
);
}// app/users/UserList.tsx (Client Component)
'use client';
import { useQuery } from '@tanstack/react-query';
export default function UserList() {
// Data is already in cache from server prefetch — no loading state
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Providers setup for Next.js App Router:
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
// Create QueryClient inside component to avoid sharing state between users
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: { staleTime: 60 * 1000 },
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}Fix 6: Custom Query Hooks and Query Factories
Organize queries into reusable hooks and factories:
// lib/queries/users.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userKeys } from './keys';
import { userApi } from '../api/users';
// Query factory for type safety and reuse
export const userQueries = {
all: () => ({
queryKey: userKeys.all,
queryFn: userApi.getAll,
}),
detail: (id: number) => ({
queryKey: userKeys.detail(id),
queryFn: () => userApi.getById(id),
enabled: id > 0,
}),
byRole: (role: string) => ({
queryKey: userKeys.byRole(role),
queryFn: () => userApi.getByRole(role),
}),
};
// Hooks wrapping useQuery
export function useUsers() {
return useQuery(userQueries.all());
}
export function useUser(id: number) {
return useQuery(userQueries.detail(id));
}
// Mutation hook
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: userApi.create,
onSuccess: (user) => {
queryClient.invalidateQueries({ queryKey: userKeys.all });
queryClient.setQueryData(userKeys.detail(user.id), user);
},
});
}
// Prefetch function for use in loaders
export async function prefetchUsers(queryClient: QueryClient) {
await queryClient.prefetchQuery(userQueries.all());
}Still Not Working?
useQuery throws “No QueryClient set” — the component calling useQuery doesn’t have a QueryClientProvider ancestor. Check your component tree. Common issue: the provider is inside a condition or lazy-loaded component, so some render paths don’t have it. Move QueryClientProvider to the root of your app, above everything else.
Query refetches on every render — the queryKey contains an object or array that’s created inline and changes reference on every render:
// WRONG — new object every render
useQuery({
queryKey: ['users', { role: userRole }], // New object each render
queryFn: fetchUsers,
});
// CORRECT — stable reference
const filters = useMemo(() => ({ role: userRole }), [userRole]);
useQuery({
queryKey: ['users', filters],
queryFn: fetchUsers,
});
// OR — primitives don't have this problem
useQuery({
queryKey: ['users', userRole], // String is stable
queryFn: fetchUsers,
});isLoading vs isPending vs isFetching confusion — in TanStack Query v5:
isPending— query has no data yet (renamed fromisLoadingin v4)isLoading—isPending && isFetching(first fetch, no cached data)isFetching— any request in flight, including background refetches
Use isLoading to show a loading spinner for the first load, and isFetching to show a subtle “updating” indicator.
For related state management issues, see Fix: React Query Infinite Refetch and Fix: Zustand Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Jotai Not Working — Atom Not Updating, Derived Atom Out of Sync, or atomWithStorage Hydration Error
How to fix Jotai state management issues — atom scope, derived atoms, async atoms with Suspense, atomWithStorage SSR, useAtomValue vs useSetAtom, and debugging stale state.
Fix: Valtio Not Working — Component Not Re-rendering, Snapshot Stale, or Proxy Mutation Not Tracked
How to fix Valtio state management issues — proxy vs snapshot, useSnapshot for React, subscribe for side effects, derived state with computed, async actions, and Valtio with React Server Components.
Fix: Zustand Not Working — Component Not Re-rendering, State Reset on Refresh, or Selector Causing Infinite Loop
How to fix Zustand state management issues — selector optimization, persist middleware, shallow comparison, devtools setup, slice pattern for large stores, and common subscription mistakes.
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.