Fix: React Query (TanStack Query) Infinite Refetching Loop
Quick Answer
How to fix React Query refetching infinitely — why useQuery keeps fetching, how object and array dependencies cause loops, how to stabilize queryKey, and configure refetch behavior correctly.
The Error
A useQuery hook triggers continuous network requests that never stop:
// This refetches on every render — network tab shows hundreds of requests
const { data } = useQuery({
queryKey: ['users', { page, filters }],
queryFn: () => fetchUsers({ page, filters }),
});Or the query refetches every few seconds even when the data hasn’t changed. Or you see this in React DevTools — the component re-renders repeatedly triggered by React Query state updates.
Why This Happens
React Query refetches in two distinct patterns:
Pattern 1 — unstable queryKey causes re-mounting loops:
React Query uses deep equality to compare query keys. But if the key contains a new object or array reference on every render, React Query sees it as a new query, cancels the current one, and starts a new fetch — causing an infinite loop.
// New object created on every render — triggers new query each time
const { data } = useQuery({
queryKey: ['users', { page: 1, sort: 'name' }], // New object reference every render
queryFn: fetchUsers,
});Pattern 2 — side effects in queryFn or onSuccess that trigger re-renders:
If the queryFn or onSuccess callback updates state, and that state change causes a re-render, and the re-render changes the query key, you get a loop.
Pattern 3 — refetchInterval set too aggressively or staleTime set to 0:
By default, React Query considers data stale immediately (staleTime: 0). Combined with refetchOnWindowFocus: true and refetchOnReconnect: true, frequent tab switching or network changes trigger continuous refetches.
Fix 1: Stabilize the queryKey
The queryKey must be stable — it should not create new object or array references on every render:
Broken — new object on every render:
function UserList({ page, filters }) {
// filters is an object — new reference on every render
const { data } = useQuery({
queryKey: ['users', { page, filters }], // ← Unstable
queryFn: () => fetchUsers(page, filters),
});
}Fixed — use primitive values in the key:
function UserList({ page, filters }) {
const { data } = useQuery({
queryKey: ['users', page, filters.status, filters.search], // ← Stable primitives
queryFn: () => fetchUsers(page, filters),
});
}Fixed — memoize complex objects used in the key:
function UserList({ page, filters }) {
// Memoize filters — only changes when values actually change
const stableFilters = useMemo(
() => ({ status: filters.status, search: filters.search }),
[filters.status, filters.search]
);
const { data } = useQuery({
queryKey: ['users', page, stableFilters],
queryFn: () => fetchUsers(page, stableFilters),
});
}Fixed — serialize the key to a stable string:
const filtersKey = JSON.stringify(filters); // Stable string representation
const { data } = useQuery({
queryKey: ['users', page, filtersKey],
queryFn: () => fetchUsers(page, filters),
});Pro Tip: Keep query keys as flat arrays of primitives whenever possible. Avoid passing entire prop objects into the key. If you need to include an object, ensure it is memoized with
useMemoor stabilized outside the component.
Fix 2: Set staleTime to Reduce Unnecessary Refetches
By default, staleTime: 0 means all data is considered stale immediately after fetching. React Query refetches stale data on window focus, component mount, and network reconnect:
// Configure globally in QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // Data is fresh for 1 minute
gcTime: 5 * 60 * 1000, // Cache kept for 5 minutes (was cacheTime in v4)
refetchOnWindowFocus: false, // Don't refetch when user switches tabs
refetchOnReconnect: true, // Keep this — useful for reconnects
retry: 1, // Retry once on failure (default 3)
},
},
});Configure per-query:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // Users data is fresh for 5 minutes
refetchOnWindowFocus: false,
refetchInterval: false, // Disable polling
});
// For real-time data — poll every 30 seconds
const { data: liveData } = useQuery({
queryKey: ['live-metrics'],
queryFn: fetchMetrics,
staleTime: 0,
refetchInterval: 30_000, // Refetch every 30 seconds
refetchIntervalInBackground: false, // Only poll when tab is active
});Fix 3: Fix Infinite Refetch Caused by queryFn Side Effects
If your queryFn triggers state changes that cause re-renders, and those re-renders change the query key, you get a loop:
Broken — queryFn sets state:
const [results, setResults] = useState([]);
const { data } = useQuery({
queryKey: ['search', query],
queryFn: async () => {
const data = await searchApi(query);
setResults(data); // ← Sets state → triggers re-render → new query key?
return data;
},
});Fixed — use data directly from React Query, don’t duplicate into state:
// React Query IS your state — don't put the result into useState too
const { data: results = [] } = useQuery({
queryKey: ['search', query],
queryFn: () => searchApi(query),
});
// Use 'results' directly — no useState neededIf you need to transform data, use select:
const { data: userNames } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.map(u => u.name), // Transform without extra state
});Fix 4: Fix Refetch Loop from useEffect + invalidateQueries
A common pattern that creates a loop: useEffect calls invalidateQueries, which triggers a refetch, which updates data, which re-runs the effect:
Broken — creates a loop:
const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
useEffect(() => {
// This runs every time 'data' changes — which happens after every refetch
queryClient.invalidateQueries({ queryKey: ['users'] }); // ← Loop!
}, [data]);Fixed — invalidate based on user actions, not query data:
const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
// Only invalidate after a mutation — not after every fetch
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
// Trigger invalidation from a user action
<button onClick={() => mutation.mutate(newUser)}>Create User</button>If you must use useEffect, add a stable dependency:
const [lastUpdated, setLastUpdated] = useState(null);
useEffect(() => {
if (externalEvent) {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
}, [externalEvent]); // Only runs when externalEvent changes — not on every data changeFix 5: Fix Refetch Loop with useMutation onSuccess
When onSuccess invalidates a query that the same component uses, and the invalidation causes a re-render that re-runs the mutation:
// Safe pattern — invalidate in onSuccess, not in useEffect watching data
const mutation = useMutation({
mutationFn: (newUser) => createUser(newUser),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
// This triggers a refetch of 'users' — but only once, not in a loop
},
onError: (error) => {
console.error('Failed to create user:', error);
},
});Optimistic updates to avoid the refetch entirely:
const mutation = useMutation({
mutationFn: createUser,
onMutate: async (newUser) => {
// Cancel any in-flight refetches
await queryClient.cancelQueries({ queryKey: ['users'] });
// Snapshot the previous value
const previousUsers = queryClient.getQueryData(['users']);
// Optimistically update
queryClient.setQueryData(['users'], (old) => [...old, newUser]);
return { previousUsers };
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(['users'], context.previousUsers);
},
onSettled: () => {
// Refetch once after mutation settles (success or error)
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});Fix 6: Disable Refetch Behaviors Globally
For apps where refetching on every focus or reconnect is unwanted:
// providers/QueryClientProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity, // Data never goes stale automatically
refetchOnWindowFocus: false, // Don't refetch on tab focus
refetchOnReconnect: false, // Don't refetch on network reconnect
refetchOnMount: false, // Don't refetch when component mounts
retry: false, // Don't retry on error
},
},
});
export function Providers({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}Note:
staleTime: Infinitymeans React Query never automatically considers data stale. Data only refetches when you explicitly callinvalidateQueriesorrefetch. This is appropriate for data that changes rarely and where you control when to refresh.
Fix 7: Debug Refetching with React Query DevTools
Install and use the DevTools to see exactly why a query is refetching:
npm install @tanstack/react-query-devtoolsimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}In the DevTools panel:
- Click a query to see its status, last fetch time, and observer count.
- The “Refetch” button manually triggers a refetch for testing.
- Watch the query state change from
fresh→stale→fetchingto understand the cycle.
Log refetch reasons:
const { data } = useQuery({
queryKey: ['users'],
queryFn: async () => {
console.log('Fetching users at:', new Date().toISOString());
return fetchUsers();
},
});Still Not Working?
Check if the component is unmounting and remounting. If the component that calls useQuery unmounts and remounts repeatedly (due to a parent re-render or router animation), the query restarts each time. Lift the query to a parent component or use keepPreviousData: true:
const { data } = useQuery({
queryKey: ['users', page],
queryFn: () => fetchUsers(page),
placeholderData: keepPreviousData, // v5 API — keeps old data while new data loads
});Check React StrictMode. In development with <StrictMode>, React intentionally double-invokes effects and renders. This can make refetching appear worse than it is in production. Test without StrictMode to confirm:
// Temporarily remove StrictMode to test
root.render(
// <React.StrictMode>
<App />
// </React.StrictMode>
);For related React data fetching issues, see Fix: React useEffect Infinite Loop and Fix: React useState Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React.memo Not Preventing Re-renders
How to fix React.memo not working — components still re-rendering despite being wrapped in memo, caused by new object/function references, missing useCallback, and incorrect comparison functions.
Fix: React.lazy and Suspense Errors (Element Type Invalid, Loading Chunk Failed)
How to fix React.lazy and Suspense errors — Element type is invalid, A React component suspended while rendering, Loading chunk failed, and lazy import mistakes with named vs default exports.
Fix: Webpack HMR (Hot Module Replacement) Not Working
How to fix Webpack Hot Module Replacement not updating the browser — HMR connection lost, full page reloads instead of hot updates, and HMR breaking in Docker or behind a proxy.
Fix: React Warning: Failed prop type
How to fix the React 'Warning: Failed prop type' error. Covers wrong prop types, missing required props, children type issues, shape and oneOf PropTypes, migrating to TypeScript, default props, and third-party component mismatches.