This episode builds your TanStack Query mental model: the difference between queries and mutations, the journey of a query key toward the cache, the concepts of observers, staleTime and gcTime, structural sharing, as well as the main components QueryClient, the provider, and the available hooks.

Before you write code, build the right mental model first. TanStack Query is not just a collection of hooks — it is a complete cache system with a clear data lifecycle. If you understand its architecture, all the configuration options in the next episodes will feel reasonable, not just memorized.
Episode 2 breaks down the two main operation models (query and mutation), how data flows from a query key toward the cache, and the components that make up this library. This is the architectural foundation of the entire series.
A query is a read operation that is equivalent to a GET request. A query is declared with two things: queryKey as a unique identity, and queryFn as the function that fetches the data. Here is the mental model:
useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
})The query result is stored in the cache under the key ["todos"]. As long as the same query is used anywhere, the same data is read from the cache — no double fetch.
A mutation is a write operation — POST, PUT, PATCH, or DELETE. A mutation is not stored in the cache the same way a query is, because it only happens once. The job of a mutation is to write data to the server, then tell the cache to update.
useMutation({
mutationFn: createTodo,
})The fundamental difference: queries are declarative and repeatable, while mutations are imperative and one-shot. useMutation is called via the mutate method in an event handler, whereas useQuery runs automatically when the component mounts. They share one thing — both run on the same global cache.
When a component calls useQuery, a flow happens that can be summarized in one diagram:
queryKey → observer → fetch → cache → componentqueryKey is the first input: it determines which cache slot is read. Every query that uses the same key shares one cache entry. This is what makes deduplication work — two components with identical keys don't perform two fetches.
Every useQuery call creates an observer. If many components use the same key, they all subscribe to the same cache entry. The library detects this and only performs one fetch for that key, then shares the result with all observers. When one observer unmounts, the others still get data from the cache without a new fetch.
Two timers that are most often misunderstood:
Data can be "fresh but unused" and "in use but stale". Both are configured separately, and episode 7 will discuss them in depth.
When the cache is updated, TanStack Query doesn't create new objects blindly. Structural sharing ensures that parts of the data structure that didn't change keep referencing the same objects, so re-renders and memoization stay efficient.
const a = { todos: [], meta: { page: 1 } }
const b = produce(a, (draft) => {
draft.meta.page = 2
})
a.todos === b.todos // true, array todos tidak disentuhThe comparison a.todos === b.todos evaluates to true because of structural sharing — the reference to the unchanged array is preserved. The produce above is only an illustration of the end result; TanStack Query's internal mechanism is different but has the same effect.
QueryClient is where all the cache and configuration live. It is created once and given to the whole application through the provider:
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
const queryClient = new QueryClient()
export function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}new QueryClient() creates one global cache instance for your application. QueryClientProvider injects that instance into the entire component tree so that all hooks can access it.
On top of QueryClient, the library provides the main hooks:
useQuery for reading a single query.useMutation for write operations.useQueries for multiple parallel queries at once.useInfiniteQuery for data loaded incrementally.useQueryClient for accessing the QueryClient from a component.All of these hooks will be broken down one by one in the episodes that follow.
Finally, there is React Query Devtools — a panel that shows the entire cache, the status of each query, the number of fetches, and buttons to invalidate manually. Devtools are your eyes and ears when debugging, and will be discussed in detail in episode 17.
Tip
From now on, get used to picturing each query as a slot in the cache with an identity defined by the query key. All TanStack Query behavior — refetch, invalidation, optimistic updates — is actually manipulation of those slots.
Episode 2 built the mental model of TanStack Query's architecture: queries for reading, mutations for writing, a global cache identified by query keys, observers that perform deduplication, and the separate staleTime and gcTime timers.
Key takeaways:
queryKey determines the cache slot; identical keys mean shared data.staleTime controls freshness; gcTime controls cache eviction.QueryClient plus the provider is the heart of this library.In the next episode, episode 3, we will discuss setting up and configuring QueryClient — how to create an instance with default options, wrap the application with QueryClientProvider, and configure staleTime, retry, refetchOnWindowFocus, and gcTime globally. This is the first step of actually writing TanStack Query code.