This episode dissects the architecture behind TanStack: the query client and cache in Query, the column system and row model in Table, state management and loaders in Router, and how the virtualizer measures the viewport in Virtual.

Episode 1 explained what TanStack solves. Now we go deeper: how these libraries work behind the scenes. Understanding the architecture means you're not just memorizing APIs, but you know the reasoning behind every behavior — for example, why queries are cached, why columns are defined as data, and why the router separates state from components.
Episode 2 dissects four core architectures: the query client and cache in TanStack Query, the column system and row model in TanStack Table, state management and loaders in TanStack Router, and how the virtualizer measures the viewport in TanStack Virtual.
These concepts are the foundation. In episode 3 you'll touch real code, and every architectural detail learned here will be used all the way to episode 23.
The core of TanStack Query is the QueryClient, which holds a QueryCache. Every useQuery call creates a Query in the cache, and the subscribing components are called QueryObservers. When several components use the same queryKey, they share a single Query — that's the basis of deduplication.
import { QueryClient } from "@tanstack/react-query"
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, retry: 1 },
},
})new QueryClient({ defaultOptions }) sets the default behavior for all queries. staleTime: 30_000 makes data considered stale after 30 seconds, and retry: 1 limits retries on error. Both are covered in detail in episode 4.
When a component mounts, the observer checks the cache: if valid data exists, it's used directly without fetching. If there's none, or it's stale, the query is refetched in the background. When done, the result is written to the cache and all subscribed observers are notified. This pattern explains why navigating between pages feels instant — the data is already warm in the cache.
QueryClientProvider delivers the queryClient to the entire component tree through React context. Without this provider, useQuery doesn't know which cache to use and throws an error immediately.
TanStack Table splits things in two: the core table that handles state and logic, and rendering that you fully control. Columns are defined as data through columnHelper, so columns can be dynamic, derived, or programmatically generated.
import { createColumnHelper } from "@tanstack/react-table"
const columnHelper = createColumnHelper()
const columns = [
columnHelper.accessor("nama", { header: "Nama" }),
columnHelper.accessor("tahun", { header: "Tahun" }),
]columnHelper.accessor("nama", { header: "Nama" }) maps a column to the nama property of the data, with a header label. This columns array is what gets passed to useReactTable in episode 6.
The row model is the engine that turns raw data into render-ready rows. getCoreRowModel is the base model; in episodes 6 and 9 you'll add sorting, filtering, grouping, and pagination models. Each model wraps the previous one in a pipeline.
TanStack Router organizes the entire application as a route tree. Each route is an object with path, loader, and component. The router holds centralized state — including the current location, search params, and loader data — and notifies components when it changes.
import { createRootRoute, createRoute, createRouter } from "@tanstack/react-router"
const rootRoute = createRootRoute()
const routeTree = rootRoute.addChildren([])
const router = createRouter({ routeTree })createRouter({ routeTree }) produces the router that gets passed to RouterProvider. The entire route tree is type-safe: wrong URLs are rejected by the compiler.
When navigation happens, the router calls the loader of the destination route, waits for its data or shows a pending state, then renders the component. Components read the loader result through useLoaderData. This lifecycle lets the router know exactly when to show loading, success, or error.
TanStack Virtual uses a Virtualizer to determine which items are visible within the viewport. The Virtualizer reads the scroll position through getScrollElement, calculates the range of visible items, and reports the total height of the scroll area.
import { useVirtualizer } from "@tanstack/react-virtual"
const virtualizer = useVirtualizer({
count: 10_000,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
})count: 10_000 declares the number of items, and estimateSize: () => 40 is the initial height estimate for each item. Only the visible items are rendered — the rest are replaced by empty blocks of the same size.
Each virtual item is given a start and size. The component positions items absolutely using a translateY transform of the start value. As a result, scrolling stays smooth even with datasets of millions of rows — full details are in episode 11.
Episode 2 dissected the architecture of the four core libraries: query client and cache in Query, core table and row model in Table, route tree and loader lifecycle in Router, and virtualizer and viewport in Virtual. All these architectures share one pattern: logic is separated from rendering.
Key takeaways:
In the next episode, episode 3, we'll start a project with TanStack — scaffolding a Vite React-TypeScript project, installing the five @tanstack/* libraries, the folder structure, and TypeScript and ESLint configuration. Get your terminal ready, because starting from episode 3 we write real code!