Learn TanStack - Full Application Patterns
Episode 15 of 24

Learn TanStack - Full Application Patterns

This episode unifies all the libraries: a dashboard with TanStack Table and Charts, coordinating query state, table state, and router state, complex UI flows with nested routing, and data-driven UI and composition.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Fourteen episodes built per-library capabilities. Now it's time to bring it all together: Query supplies the data, Table presents it, Charts visualizes it, and Router manages navigation. Episode 15 builds full application patterns that become the blueprint for your projects.

Episode 15 covers a dashboard with Table and Charts, coordinating query state, table state, and router state, complex UI flows with nested routing, and data-driven UI and composition.

This is where the TanStack philosophy pays off: because every library is headless and independent, combining them is a matter of wiring state together, not fighting APIs.

Dashboard with TanStack Table and Charts

One Data Source, Many Views

A dashboard reads one dataset and displays it as both a table and a chart. The data source is a single query; both just consume the same cache:

JSDashboard dengan table dan chart
import { Chart } from "@tanstack/react-charts"
import { useQuery } from "@tanstack/react-query"
 
function Dashboard() {
  const { data } = useQuery({
    queryKey: ["penjualan"],
    queryFn: ambilPenjualan,
  })
 
  const chartData = [
    {
      id: "penjualan",
      label: "Penjualan per Bulan",
      data: data.points,
    },
  ]
 
  return (
    <div>
      <Chart options={{
        data: chartData,
        primaryAxis: { getValue: (p) => p.bulan },
        secondaryAxes: [{ getValue: (p) => p.total }],
      }} />
      <TabelPenjualan rows={data.rows} />
    </div>
  )
}

useQuery fetches penjualan once. The same data is reshaped for Chart and passed to the table. Because the cache is shared, modifying data in the table is instantly reflected in the chart — with no manual sync.

Coordinating Query, Table, and Router State

One URL for Every Condition

Filters, pages, and table searches are best stored in the URL. The router provides search params, and queries use search params as part of the queryKey:

JSSearch params sebagai filter tabel
const filterRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "laporan",
  validateSearch: (search) => ({
    halaman: search.halaman ?? 1,
    cari: search.cari ?? "",
  }),
  loader: async ({ context, search }) => {
    return context.queryClient.ensureQueryData({
      queryKey: ["laporan", search],
      queryFn: () => ambilLaporan(search),
    })
  },
  component: LaporanComponent,
})

validateSearch defines the shape of the search params with defaults. The loader uses search as part of the queryKey, so every filter-and-page combination gets its own cache. The table then reads this search to populate filters and pagination — the URL becomes the single source of truth.

Complex UI Flows with Nested Routing

Layered Layouts for Complex Flows

Nested routing breaks a complicated flow into small routes, each with its own loader and error handling:

JSNested routes untuk flow bertingkat
const checkoutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "checkout",
  component: LayoutCheckout,
})
 
const alamatRoute = createRoute({
  getParentRoute: () => checkoutRoute,
  path: "alamat",
  component: FormAlamat,
})
 
const pembayaranRoute = createRoute({
  getParentRoute: () => checkoutRoute,
  path: "pembayaran",
  loader: ({ context }) =>
    context.queryClient.ensureQueryData({ queryKey: ["metode"], queryFn: ambilMetodePembayaran }),
  component: FormPembayaran,
})

alamatRoute and pembayaranRoute are children of checkoutRoute. The checkout layout holds a stepper and an <Outlet />, while each step loads its own data. Navigating between steps changes the URL and triggers each step's loader — a long flow becomes measurable.

Data-driven UI and Composition

UI Built From Data

The last pattern: let data determine the UI structure. The table reads column definitions as data, the chart reads series as data, the router reads the route tree as data. It's all composition:

JSKomposisi data-driven
function HalamanTabel({ definisiKolom, rows }) {
  const table = useReactTable({
    data: rows,
    columns: definisiKolom,
    getCoreRowModel: getCoreRowModel(),
  })
 
  return <RenderTable table={table} />
}

definisiKolom is passed in as a prop from data — not hard-coded inside the component. This way, different pages can use the same table component with different columns. That's the essence of data-driven UI: logic and display are separated, and data drives both.

Info

When holding search params in the URL, don't forget to constrain their values in validateSearch. This prevents unexpected values from entering the queryKey and corrupting the cache.

Conclusion

Episode 15 wrapped up full application patterns: a dashboard uniting Query, Table, and Charts, state coordinated through search params in the URL, complex flows with nested routing, and data-driven composition that makes UI flexible.

Key takeaways:

  • One query can feed both a table and a chart.
  • Search params in the URL become the single source of truth for filters.
  • The queryKey includes the search so the cache is separate per condition.
  • Nested routing breaks complex flows into small routes.
  • Per-step loaders keep each stage with its own data.
  • Data-driven composition separates logic from display.

In the next episode, episode 16, we'll discuss testing and quality assurance — unit testing Query hooks, testing Router navigation, testing table and virtual list rendering, and integration testing with a mock server. Your app is about to be properly tested!

Learn TanStack - Full Application Patterns | Learn TanStack