Use when fetching data from APIs, managing server state, implementing loading and error states, or choosing a data fetching strategy. Prevents the common mistake of fetching in useEffect without proper caching or race condition handling. Covers TanStack Query, useQuery, useMutation, Suspense, React 19 use() hook, caching, optimistic updates, pagination. Keywords: TanStack Query, useQuery, useMutation, fetch, use(), server state, Axios, API call, fetch data, loading spinner, how to call API, server state, caching API..
Instrucciones de origen · Vista previa de solo lectura
name
react-impl-data-fetching
description
Use when fetching data from APIs, managing server state, implementing loading and error states, or choosing a data fetching strategy. Prevents the common mistake of fetching in useEffect without proper caching or race condition handling. Covers TanStack Query, useQuery, useMutation, Suspense, React 19 use() hook, caching, optimistic updates, pagination. Keywords: TanStack Query, useQuery, useMutation, fetch, use(), server state, Axios, API call, fetch data, loading spinner, how to call API, server state, caching API..
license
MIT
compatibility
Designed for Claude Code. Requires React 18.x or 19.x with TypeScript.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
react-impl-data-fetching
Quick Reference
Data Fetching Strategy Decision Tree
Need to fetch data from an API?
├── Server state (remote data, shared, async)?
│ ├── YES → Use TanStack Query (RECOMMENDED)
│ │ ├── Read data → useQuery / useSuspenseQuery
│ │ ├── Write data → useMutation + invalidateQueries
│ │ └── Paginated → useInfiniteQuery
│ └── Using React Router? → Loader functions (route-level)
├── React 19 with Suspense architecture?
│ └── use() hook for reading cached promises
└── Simple one-off fetch (rare)?
└── useEffect with cleanup (LAST RESORT — see anti-patterns)
TanStack Query Setup
import { QueryClient, QueryClientProvider } from'@tanstack/react-query';
const queryClient = newQueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes before data is considered stalegcTime: 1000 * 60 * 30, // 30 minutes before inactive data is garbage collectedretry: 3, // Retry failed requests 3 timesrefetchOnWindowFocus: true, // Refetch when user returns to tab
},
},
});
functionApp() {
return (
<QueryClientProviderclient={queryClient}><Router /></QueryClientProvider>
);
}
Critical Warnings
NEVER fetch data in useEffect without a cleanup flag -- race conditions cause stale responses to overwrite fresh ones. Use TanStack Query instead.
NEVER manage server state with useState + useEffect -- you lose caching, deduplication, background refresh, and error retry for free.
NEVER create a new promise inside a component's render body when using React 19 use() -- the promise is recreated every render, causing infinite loops. ALWAYS cache the promise outside render.
NEVER call queryClient.invalidateQueries() without awaiting mutation completion -- invalidation before the server processes the mutation returns stale data.
ALWAYS wrap your app in QueryClientProvider with a QueryClient instance created OUTSIDE the component -- creating it inside causes a new client every render, destroying all cache.
ALWAYS use array-based queryKey values -- TanStack Query uses structural sharing for cache matching. Include all variables the query depends on.
useQuery: Reading Server Data
import { useQuery } from'@tanstack/react-query';
interfaceUser {
id: number;
name: string;
email: string;
}
functionUserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error, isFetching } = useQuery<User>({
queryKey: ['user', userId], // Cache key (MUST include all variables)queryFn: () =>fetchUser(userId), // Fetch function (MUST return a promise)enabled: userId > 0, // Only fetch when condition is truestaleTime: 1000 * 60 * 5, // Data fresh for 5 minutesgcTime: 1000 * 60 * 30, // Keep in cache 30 minutes after unmountselect: (data) => data.name, // Transform response (only re-renders on change)placeholderData: { id: 0, name: 'Loading...', email: '' },
});
if (isLoading) return<Skeleton />;
if (isError) return<ErrorMessageerror={error} />;
return<div>{data.name}</div>;
}
Key useQuery Options
Option
Type
Purpose
queryKey
unknown[]
Unique cache key -- include ALL dependent variables
queryFn
() => Promise<T>
Function that fetches data
enabled
boolean
Disable query until condition is met
staleTime
number
Milliseconds before data is considered stale
gcTime
number
Milliseconds before inactive cache is garbage collected
import { useSuspenseQuery } from'@tanstack/react-query';
import { Suspense } from'react';
import { ErrorBoundary } from'react-error-boundary';
functionUserList() {
const { data } = useSuspenseQuery<User[]>({
queryKey: ['users'],
queryFn: fetchUsers,
});
// data is ALWAYS defined -- loading/error handled by Suspense/ErrorBoundaryreturn (
<ul>
{data.map((user) => (
<likey={user.id}>{user.name}</li>
))}
</ul>
);
}
// Parent handles loading and error states declarativelyfunctionUsersPage() {
return (
<ErrorBoundaryfallback={<p>Failed to load users.</p>}>
<Suspensefallback={<Skeleton />}>
<UserList /></Suspense></ErrorBoundary>
);
}
ALWAYS wrap useSuspenseQuery components in both <Suspense> and <ErrorBoundary> -- useSuspenseQuery throws promises (for Suspense) and errors (for ErrorBoundary).
React 18: use() is NOT available. Use useSuspenseQuery from TanStack Query for Suspense-based data fetching.
React 19: use() can read promises and context. ALWAYS ensure the promise is cached (via cache(), useMemo, or module scope) to prevent re-creation on every render.
Caching Strategy
staleTime vs gcTime
Setting
Controls
Default
Recommendation
staleTime
How long data is "fresh" (no refetch)
0 (always stale)
Set per query based on data volatility
gcTime
How long inactive cache is kept in memory
5 min
ALWAYS >= staleTime
Query Invalidation
const queryClient = useQueryClient();
// Invalidate a specific query
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// Invalidate all queries starting with 'users'
queryClient.invalidateQueries({ queryKey: ['users'] });
// Invalidate everything
queryClient.invalidateQueries();