Fifty components mounted against the same endpoint fire fifty identical calls, and each one keeps its own little loading state. TanStack Query turns data fetching into shared cache with explicit rules: the key defines identity, time defines freshness, mutations define invalidation. Kent C. Dodds, in the article Application State Management (kentcdodds.com, 2018), separates server state from client state; the library takes the first.
The key as a contract
queryKey: ['todos', userId] declares the data's identity. Two screens with the same key read the same cache. Change a parameter, change the key, and the cache reserves new space. staleTime defines how long data stays fresh without a refetch; gcTime defines how long an unused cache entry stays in memory. The TanStack Query documentation (2025) treats deterministic key serialization as part of the contract: same object, same property order, same key.
Dedupe on mount
Three instances mounted together with the same key perform one network call: the library deduplicates the in-flight request. Refetch on window focus runs by default and keeps data fresh in a tab that sat open. Suspense mode hands you React's Suspense instead of a loading hook; the query promise propagates to the nearest boundary.
Mutation and invalidation
useMutation takes an onSuccess that calls invalidateQueries with the affected list's prefix. An optimistic update writes to the cache before the response and restores the previous state when the call fails. Two screens that depend on the same data invalidate together; await queryClient.invalidateQueries holds the toast until the refetch finishes. A mutation that changes a single cache item by equality uses setQueryData instead of a full refetch.
Where does global state fit?
Server state lives in the query cache. Theme, open drawer, and UI filters live in zustand or context. The session token lives in an httpOnly cookie, outside the serialized cache. prefetchQuery on link hover warms the cache before navigation; queryClient.getQueryData reads cached data inside the loader. The boundary between the two categories becomes a team rule: anyone who puts remote data into global context reimplements half the library, concurrency bug included.
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk