Data Fetching Without the useEffect Dance
The useState + useEffect pattern for fetching data is 15 lines of incidental complexity around 1 line of actual work. TanStack Query replaces all of it with a single hook call and adds caching, retries, and deduplication for free.
There is one line that actually does the work:
const flights = await searchFlights(params);Everything else in the typical fetch-with-effect pattern is noise: loading state, error state, cleanup to prevent stale responses, an abort controller, and an empty dependency array that you hope is correct.
The work is one line. The infrastructure is fifteen.
The Pattern Everyone Has Written
function FlightResults({ params }: { params: FlightSearch }) {
const [flights, setFlights] = useState<Flight[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
setError(null);
const controller = new AbortController();
searchFlights(params, { signal: controller.signal })
.then(data => { if (!cancelled) setFlights(data); })
.catch(err => { if (!cancelled && err.name !== 'AbortError') setError(err.message); })
.finally(() => { if (!cancelled) setIsLoading(false); });
return () => {
cancelled = true;
controller.abort();
};
}, [params.destination, params.startDate, params.endDate]);
}This has nothing to do with flights. It is the same boilerplate for every async value in the application -- textbook incidental complexity. And it does not even cover caching -- search the same route twice and both requests go to the network.
TanStack Query Is a Cache, Not a Fetch Library
The mental model that helps: TanStack Query does not fetch data. It manages a local cache that happens to work very well with async data. This is a different concern from avoiding cascading effects -- that post fixes how you react to data changes; this one eliminates the boilerplate of getting data in the first place.
useQuery says: "Give me whatever is in the cache for this key. If there is nothing cached, or if the cache is stale, run this function to populate it."
import { useQuery } from '@tanstack/react-query';
function FlightResults({ params }: { params: FlightSearch }) {
const { data: flights, isLoading, error } = useQuery({
queryKey: ['flights', params],
queryFn: () => searchFlights(params),
});
}Three lines replace eighteen. Loading state, error state, and cleanup come from the hook. AbortController handling is built in.
What You Get for Free
Caching. Search Tokyo → Paris. Go back and search Tokyo → Paris again. The second search returns instantly from cache.
Deduplication. Two components that call useQuery with the same key at the same time share one network request, not two.
Background refetching. When the user switches tabs and returns, TanStack Query silently refetches to check for fresh data.
Automatic retries. Failed requests retry three times by default before surfacing the error.
These are features most teams eventually want to build. TanStack Query has them already.
ExpanduseEffect pattern: 15 lines of loading/error/cleanup boilerplate wrapping 1 line of fetch; useQuery: 3 lines, same 1 line of fetch, caching and retries automatic
The Query Key
The query key is how TanStack Query identifies a cached result. Two queries with the same key share the same cache entry.
// Different keys → different cache entries
useQuery({ queryKey: ['flights', { destination: 'Tokyo', date: '2026-07' }], queryFn: ... })
useQuery({ queryKey: ['flights', { destination: 'Osaka', date: '2026-07' }], queryFn: ... })
// Same key → same cache entry, no duplicate request
useQuery({ queryKey: ['flights', { destination: 'Tokyo', date: '2026-07' }], queryFn: ... })
useQuery({ queryKey: ['flights', { destination: 'Tokyo', date: '2026-07' }], queryFn: ... })Include every parameter that changes the response in the key. TanStack Query uses deep equality, so passing the full params object is fine.
Status vs Booleans
TanStack Query exposes both:
const { status, isLoading, isPending, isFetching, error, data } = useQuery({ ... });
// status is a discriminated union: 'pending' | 'error' | 'success'
if (status === 'pending') return <Spinner />;
if (status === 'error') return <ErrorMessage message={error.message} />;
return <FlightList flights={data} />;isPending is true on the very first fetch when there is no data yet. isFetching is true any time a request is in flight, including background refetches. isLoading (deprecated name in v5, now isPending) -- use isPending in new code.
When Not to Use TanStack Query
Server components. When you can await a data call directly inside a server component, do that. The data never touches the client bundle, there is no loading state to manage, and Suspense handles the fallback:
// Server component -- no TanStack Query needed
async function ItineraryPage({ id }: { id: string }) {
const itinerary = await getItinerary(id); // direct await
return <ItineraryView data={itinerary} />;
}Use TanStack Query for client components that need to fetch data interactively -- search-as-you-type, lazy-loaded panels, mutations triggered by user action.
The Essentials
- The
useState+useEffectpattern for fetching is 15 lines of incidental complexity. TanStack Query reduces it to 3 lines and handles cancellation, deduplication, retries, and caching automatically. - The query key is the cache key. Include every parameter that affects the response. Same key = same cache entry = shared request.
- TanStack Query is a cache library, not a fetch library. It does not care how data is fetched -- it cares about storing, invalidating, and re-validating the result. This is what makes features like instant second searches possible.
Further Reading and Watching
- TanStack Query Docs -- Overview: Start here. The mental model section explains the cache-first approach better than any third-party explanation.
- You Might Not Need an Effect -- React Docs: Covers fetching data in effects specifically and why it is an anti-pattern -- with the React team's recommended alternatives including Suspense and data fetching libraries.
- React Query in 100 Seconds -- Fireship: Fast visual overview of the core
useQuerypattern and cache behavior. Note: verify this YouTube link before publishing.
Keep reading