Prev Next

Web / TanStack Interview Questions

1. What is TanStack and what problem does it solve for frontend developers? 2. What is TanStack Query and what core problems does it solve over plain fetch in Vue? 3. How do you set up TanStack Query in a Vue application? 4. How should you design queryKeys in TanStack Query and why do they matter? 5. What is the difference between staleTime and gcTime in TanStack Query? 6. What are the most useful useQuery options beyond queryKey and queryFn in Vue? 7. What is useMutation and how do you use it to submit data in Vue? 8. How do you write dependent (sequential) queries in TanStack Query with Vue? 9. How do you implement pagination and infinite scroll with TanStack Query? 10. How do you prefetch data in TanStack Query to speed up navigation? 11. What is TanStack Router and how does it differ from Vue Router? 12. How do you define routes in TanStack Router with Vue? 13. How do you use URL search params as typed state in TanStack Router? 14. What are route loaders in TanStack Router and how do they integrate with TanStack Query? 15. What is TanStack Table and how do you create a basic table in Vue? 16. How do you add client-side sorting and column filtering to a TanStack Table in Vue? 17. How do you implement pagination with TanStack Table? 18. How do you add column visibility toggling and row selection to a TanStack Table? 19. What is TanStack Form and how do you build a basic form in Vue? 20. How do you add synchronous and asynchronous field validation in TanStack Form? 21. How do you manage dynamic field arrays in TanStack Form? 22. What is TanStack Virtual and when should you use it? 23. How do you handle variable-height rows in TanStack Virtual? 24. How do you combine useInfiniteQuery with useVirtualizer for an infinitely scrolling virtualised list? 25. How do you implement optimistic updates with TanStack Query? 26. What are the status and fetchStatus values in TanStack Query v5 and how do they drive UI? 27. How do you create links and perform programmatic navigation in TanStack Router with Vue? 28. How does TanStack Router handle route-level errors and loading states? 29. How do you integrate TanStack Table with server-side sorting, filtering and pagination? 30. What developer tools does TanStack provide and how do you add them in Vue? 31. What are query key factories and why are they recommended for large Vue apps? 32. How do you implement cross-field (form-level) validation and handle submission in TanStack Form? 33. How do you pass variables to a mutation and use them across lifecycle callbacks? 34. How do you virtualise a horizontal list or two-dimensional grid with TanStack Virtual? 35. What are the most common TanStack Query mistakes in Vue and how do you avoid them? 36. How do you implement protected routes and auth redirects in TanStack Router? 37. How do you render custom Vue components inside TanStack Table cells? 38. How do the TanStack libraries compose into a full application stack, and what is TanStack Start?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. What is TanStack and what problem does it solve for frontend developers?

TanStack is a collection of open-source, framework-agnostic libraries by Tanner Linsley that eliminate the most complex, repetitive concerns in modern web apps. The suite includes TanStack Query (server-state caching), TanStack Router (type-safe routing), TanStack Table (headless table logic), TanStack Form (form-state management), and TanStack Virtual (virtualised list/grid rendering).

All libraries are headless — they ship logic and state but zero UI or CSS, so teams retain full markup control. Every library works with Vue, React, Solid, Svelte, Angular and Qwik through first-party adapters.

TanStack Suite
LibraryResponsibilityVue package
TanStack Query v5Server state, caching, background refetching@tanstack/vue-query
TanStack Router v1Type-safe routing, search params, loaders@tanstack/vue-router
TanStack Table v8Sorting, filtering, pagination logic@tanstack/vue-table
TanStack Form v1Form state, validation, field arrays@tanstack/vue-form
TanStack Virtual v3Virtualised row/column rendering@tanstack/vue-virtual
What does 'headless' mean for TanStack libraries?
Which library handles server-fetched data with automatic caching?
2. What is TanStack Query and what core problems does it solve over plain fetch in Vue?

TanStack Query wraps async data-fetching functions and provides automatic caching, background refetching, request deduplication, loading/error state, and cache invalidation — with zero manual cache code.

<!-- WITHOUT TanStack Query -->
<script setup lang="ts">
import { ref, onMounted } from "vue"
const user=ref(null), loading=ref(true), error=ref(null)
onMounted(async()=>{
  try{ user.value=await fetch("/api/user/1").then(r=>r.json()) }
  catch(e){ error.value=e } finally{ loading.value=false }
})
</script>

<!-- WITH TanStack Query -->
<script setup lang="ts">
import { useQuery } from "@tanstack/vue-query"
const { data:user, isPending, isError } = useQuery({
  queryKey: ["user",1],
  queryFn:  ()=>fetch("/api/user/1").then(r=>r.json()),
})
</script>
Plain fetch vs TanStack Query
ConcernPlain fetchTanStack Query
Loading stateManual ref(true/false)isPending, isFetching auto-managed
Error stateManual try/catchisError, error auto-managed
CachingNone — re-fetches every mountCached by queryKey, configurable TTL
DeduplicationDuplicate concurrent requestsSingle in-flight request per key
Background refreshNoneAuto on window focus / reconnect
InvalidationManualqueryClient.invalidateQueries()
Main caching advantage of TanStack Query over onMounted fetch?
What does isPending mean in a useQuery result?
3. How do you set up TanStack Query in a Vue application?

Create a QueryClient once in main.ts and register it via VueQueryPlugin. The client holds the cache and default options, and is accessible in any component via useQueryClient().

// main.ts
import { createApp } from "vue"
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"
import App from "./App.vue"

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000*60*5,  // 5 min fresh
      gcTime:    1000*60*10, // 10 min in cache after unmount
      retry: 2,
      refetchOnWindowFocus: true,
    },
  },
})
createApp(App).use(VueQueryPlugin,{queryClient}).mount("#app")

// Any component
import { useQueryClient } from "@tanstack/vue-query"
const qc = useQueryClient()
async function save(){
  await saveUser(form)
  await qc.invalidateQueries({ queryKey:["users"] })
}
Purpose of QueryClient?
What does qc.invalidateQueries({queryKey:['users']}) do?
4. How should you design queryKeys in TanStack Query and why do they matter?

The queryKey is a serialisable array uniquely identifying a cached query. TanStack Query uses it to look up the cache, trigger refetches when it changes, and scope invalidations. Organise keys from most-general to most-specific.

// Hierarchical — enables partial-match invalidation
const allUsers  = ["users"]
const userById  = ["users",{id:42}]
const userPosts = ["users",{id:42},"posts"]

// Invalidate ALL user-related queries at once
queryClient.invalidateQueries({ queryKey:["users"] })

// Key-factory pattern — single source of truth, TypeScript catches typos
export const userKeys = {
  all:    ()          => ["users"]           as const,
  detail: (id:number) => ["users",{id}]      as const,
  posts:  (id:number) => ["users",{id},"posts"] as const,
}

const { data } = useQuery({
  queryKey: userKeys.detail(userId.value),
  queryFn:  ()=>fetchUser(userId.value),
})
Why must a queryKey include all variables used inside queryFn?
Calling invalidateQueries(['users']) when ['users',{id:1}] and ['users',{id:2}] exist — what happens?
5. What is the difference between staleTime and gcTime in TanStack Query?

These are the most commonly confused options — they govern completely different lifecycle phases.

staleTime vs gcTime
OptionControlsDefault
staleTimeHow long cached data is 'fresh' — no background refetch while fresh0 ms (immediately stale)
gcTimeHow long an unused cache entry (no subscribers) stays in memory before GC5 minutes
const { data } = useQuery({
  queryKey: ["config"],
  queryFn:  fetchConfig,
  staleTime: 1000*60*10, // fresh 10 min — no background refetch
  gcTime:    1000*60*30, // kept 30 min after last subscriber leaves
})
// t=0:    fetched. FRESH.
// t=8min: remount. Still FRESH — cache hit, no network call.
// t=11min: remount. STALE — serves cache, refetches in background.
// t=35min: unmounted 30 min — cache REMOVED by GC.

Tip: staleTime:Infinity means data never goes stale automatically — only explicit invalidateQueries() refreshes it. Ideal for app config or country lists.

Component remounts 3 min after last fetch, staleTime=5min. What happens?
What does gcTime control?
6. What are the most useful useQuery options beyond queryKey and queryFn in Vue?

TanStack Query exposes many options; the following solve the most common real-world patterns.

<script setup lang="ts">
import { computed, ref } from "vue"
import { useQuery, keepPreviousData } from "@tanstack/vue-query"

const props = defineProps<{ userId: number|null }>()

// enabled: skip query when userId is null
const { data:user } = useQuery({
  queryKey: computed(()=>["user",props.userId]),
  queryFn:  ()=>fetchUser(props.userId!),
  enabled:  computed(()=>props.userId!==null),
})

// select: transform result; re-render only when selected slice changes
const { data:userName } = useQuery({
  queryKey: ["user",1],
  queryFn:  ()=>fetchUser(1),
  select:   d=>d.name,
})

// refetchInterval: poll every 5 s
const { data:price } = useQuery({
  queryKey: ["stock","AAPL"],
  queryFn:  ()=>fetchStockPrice("AAPL"),
  refetchInterval: 5000,
  refetchIntervalInBackground: false,
})

// placeholderData: show previous page while next loads
const page = ref(1)
const { data:posts } = useQuery({
  queryKey: computed(()=>["posts",page.value]),
  queryFn:  ()=>fetchPosts(page.value),
  placeholderData: keepPreviousData,
})
</script>
What does enabled:false on a useQuery do?
What does the select option do?
7. What is useMutation and how do you use it to submit data in Vue?

useMutation handles write operations (POST/PUT/DELETE). Unlike useQuery it does not run automatically — you call the returned mutate function explicitly. Lifecycle callbacks handle side effects like cache invalidation and notifications.

<script setup lang="ts">
import { useMutation, useQueryClient } from "@tanstack/vue-query"
const qc = useQueryClient()

const { mutate:createPost, isPending, isError, error } = useMutation({
  mutationFn: (post:{title:string;body:string}) =>
    fetch("/api/posts",{
      method:"POST",
      headers:{"Content-Type":"application/json"},
      body:JSON.stringify(post),
    }).then(r=>r.json()),

  onSuccess: ()=>{ qc.invalidateQueries({queryKey:["posts"]}) },
  onError:   (err)=>{ console.error(err) },
  onSettled: ()=>{ /* runs after success OR error */ },
})
</script>

<template>
  <button :disabled="isPending"
          @click="createPost({title:'Hi',body:'World'})">
    {{ isPending ? "Saving..." : "Save Post" }}
  </button>
  <p v-if="isError">{{ error.message }}</p>
</template>
Key difference between useQuery and useMutation?
When should you call invalidateQueries in onSuccess?

8. How do you write dependent (sequential) queries in TanStack Query with Vue?

Use the enabled option with a computed() boolean. In Vue every option that reads reactive state must be wrapped in computed(); otherwise TanStack Query captures the value once at setup and never updates.

<script setup lang="ts">
import { computed } from "vue"
import { useQuery } from "@tanstack/vue-query"

// Step 1: fetch current session
const { data:session } = useQuery({
  queryKey: ["session"],
  queryFn:  fetchCurrentSession,
})

// Step 2: only fetch projects once session is available
const userId = computed(()=>session.value?.userId)

const { data:projects } = useQuery({
  queryKey: computed(()=>["projects",{userId:userId.value}]),
  queryFn:  ()=>fetchProjectsByUser(userId.value!),
  enabled:  computed(()=>userId.value!==undefined),
})
</script>

Rule: wrap queryKey and enabled in computed() whenever they read reactive state — plain values are evaluated once at setup and never react to changes.

Why wrap queryKey/enabled in computed() in Vue TanStack Query?
State of a query when enabled=false and it has never fetched?
9. How do you implement pagination and infinite scroll with TanStack Query?

Standard pagination: plain useQuery with a page ref in the key plus keepPreviousData to avoid blank states. Infinite scroll: useInfiniteQuery accumulates pages automatically.

<script setup lang="ts">
import { ref, computed } from "vue"
import { useQuery, useInfiniteQuery, keepPreviousData } from "@tanstack/vue-query"

// Pattern 1 — paginated table
const page = ref(1)
const { data:postsPage } = useQuery({
  queryKey: computed(()=>["posts",{page:page.value}]),
  queryFn:  ()=>fetchPosts({page:page.value,limit:10}),
  placeholderData: keepPreviousData,
})

// Pattern 2 — infinite / "Load more"
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey:         ["posts","infinite"],
  queryFn:          ({pageParam})=>fetchPosts({page:pageParam,limit:10}),
  initialPageParam: 1,
  getNextPageParam: (last,all)=>last.hasMore ? all.length+1 : undefined,
})
const allPosts = computed(()=>data.value?.pages.flatMap(p=>p.items)??[])
</script>
<template>
  <article v-for="p in allPosts" :key="p.id">{{ p.title }}</article>
  <button @click="fetchNextPage()" :disabled="!hasNextPage||isFetchingNextPage">
    {{ isFetchingNextPage?"Loading...":"Load more" }}
  </button>
</template>
What does getNextPageParam return to signal no more pages?
What does keepPreviousData do in a paginated useQuery?
10. How do you prefetch data in TanStack Query to speed up navigation?

Prefetching loads data into the cache before it is needed — on hover, on route entry, or alongside the current page. When the user navigates, data is already cached and renders instantly.

<script setup lang="ts">
import { useQueryClient } from "@tanstack/vue-query"
import { ref, watchEffect } from "vue"
const qc = useQueryClient()

// Prefetch on hover
async function prefetchPost(id:number){
  await qc.prefetchQuery({
    queryKey: ["post",id],
    queryFn:  ()=>fetchPost(id),
    staleTime: 1000*60, // skip if cached < 1 min
  })
}

// Prefetch next page while user reads current
const page=ref(1)
watchEffect(()=>{
  qc.prefetchQuery({
    queryKey: ["posts",page.value+1],
    queryFn:  ()=>fetchPosts(page.value+1),
  })
})
</script>
<template>
  <RouterLink v-for="p in data?.items" :key="p.id"
    :to="`/posts/${p.id}`"
    @mouseenter="prefetchPost(p.id)">{{ p.title }}</RouterLink>
</template>
What makes prefetchQuery different from useQuery?
When does prefetchQuery skip the network request?
11. What is TanStack Router and how does it differ from Vue Router?

TanStack Router is a fully type-safe client-side router with end-to-end TypeScript inference for route params, search params, and loaders. A misspelled param or a navigation to a non-existent route is a compile-time error, not a runtime bug.

TanStack Router vs Vue Router
FeatureTanStack RouterVue Router
Type safetyFull inference on params, search params, loadersManual typing via module augmentation
Search paramsFirst-class typed, validated, serialised statePlain string parsing, manual validation
Data loadingBuilt-in route loaders with pending/error UIManual Pinia/Query integration
File-based routingYes (optional)Not built-in
// main.ts
import { createRouter } from "@tanstack/vue-router"
import { rootRoute }    from "./routes/__root"

export const router = createRouter({
  routeTree:      rootRoute,
  defaultPreload: "intent", // prefetch on hover
})
createApp(App).use(router).mount("#app")

// Type-safe navigation
import { useNavigate } from "@tanstack/vue-router"
const navigate = useNavigate()
navigate({ to:"/users/$userId", params:{ userId:"42" } })
// TS error if "userId" is not a valid param for that route
Key type-safety advantage of TanStack Router over Vue Router?
How does TanStack Router treat search params differently?
12. How do you define routes in TanStack Router with Vue?

Use createRootRoute() for the layout root and createRoute() for each page. Routes are assembled into a typed tree and passed to createRouter().

// routes/__root.ts
import { createRootRoute, RouterOutlet } from "@tanstack/vue-router"
import { h } from "vue"
export const rootRoute = createRootRoute({
  component: ()=>h("div",null,[h(RouterOutlet)]),
})

// routes/users/$userId.ts — $ marks a dynamic segment
import { createRoute } from "@tanstack/vue-router"
import UserDetailPage  from "../pages/UserDetailPage.vue"
export const userDetailRoute = createRoute({
  getParentRoute: ()=>rootRoute,
  path:           "/users/$userId",
  component:      UserDetailPage,
})

// Assemble the tree
const routeTree = rootRoute.addChildren([userDetailRoute])
export const router = createRouter({ routeTree })

// Access typed params in a component
import { useParams } from "@tanstack/vue-router"
const { userId } = useParams({ from:"/users/$userId" })
// userId typed as string; TS error on wrong param name
What convention marks a path segment as dynamic in TanStack Router?
What does getParentRoute return?
13. How do you use URL search params as typed state in TanStack Router?

Declare a validateSearch schema on the route. Search params become typed, validated, URL-persistent state — perfect for filters, sort order, and pagination that must survive refresh and be shareable via URL.

import { createRoute } from "@tanstack/vue-router"
import { z }           from "zod"

export const postsRoute = createRoute({
  getParentRoute: ()=>rootRoute,
  path:           "/posts",
  component:      PostsPage,
  validateSearch: z.object({
    page:   z.number().int().positive().default(1),
    status: z.enum(["all","published","draft"]).default("all"),
    q:      z.string().optional(),
  }).parse,
})

// In the component
import { useSearch, useNavigate } from "@tanstack/vue-router"
const search   = useSearch({ from:"/posts" })
// Typed: { page:number, status:"all"|"published"|"draft", q?:string }
const navigate = useNavigate()

function setPage(n:number){
  navigate({ to:"/posts", search: prev=>({...prev, page:n}) })
}
Main advantage of storing filter/pagination state in URL search params?
What does passing a function to the 'search' option in navigate() do?
14. What are route loaders in TanStack Router and how do they integrate with TanStack Query?

Route loaders run before the component renders. Using queryClient.ensureQueryData() inside a loader pre-populates the Query cache so the component renders synchronously with no loading spinner.

import { createRoute }  from "@tanstack/vue-router"
import { queryClient }  from "../queryClient"
import { userKeys }     from "../queries/userKeys"

export const userDetailRoute = createRoute({
  getParentRoute: ()=>rootRoute,
  path:           "/users/$userId",
  component:      UserDetailPage,
  loader: async ({ params })=>{
    await queryClient.ensureQueryData({
      queryKey: userKeys.detail(Number(params.userId)),
      queryFn:  ()=>fetchUser(Number(params.userId)),
    })
    // Component will find data already in cache — renders instantly
  },
})

// In UserDetailPage.vue
import { useParams } from "@tanstack/vue-router"
import { useQuery }  from "@tanstack/vue-query"
const { userId } = useParams({ from:"/users/$userId" })
const { data:user } = useQuery({
  queryKey: userKeys.detail(Number(userId)),
  queryFn:  ()=>fetchUser(Number(userId)),
})
How does ensureQueryData differ from prefetchQuery?
Why prefetch in a loader rather than inside the component?
15. What is TanStack Table and how do you create a basic table in Vue?

TanStack Table v8 is a headless table engine handling sorting, filtering, pagination, selection, grouping and virtualisation. It returns a typed table object with methods and state you use in your own markup — it renders nothing itself.

<script setup lang="ts">
import {
  useVueTable, createColumnHelper, getCoreRowModel,
  getSortedRowModel, FlexRender,
} from "@tanstack/vue-table"
import { ref } from "vue"

type Person = { id:number; name:string; age:number }
const data = ref<Person[]>([
  { id:1, name:"Alice", age:30 },
  { id:2, name:"Bob",   age:25 },
])

const ch = createColumnHelper<Person>()
const columns = [
  ch.accessor("name", { header:"Name", enableSorting:true }),
  ch.accessor("age",  { header:"Age",  enableSorting:true }),
]

const table = useVueTable({
  get data(){ return data.value },
  columns,
  getCoreRowModel:   getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
})
</script>
<template>
  <table>
    <thead>
      <tr v-for="hg in table.getHeaderGroups()" :key="hg.id">
        <th v-for="h in hg.headers" :key="h.id"
            @click="h.column.getToggleSortingHandler()?.($event)">
          <FlexRender :render="h.column.columnDef.header" :props="h.getContext()" />
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in table.getRowModel().rows" :key="row.id">
        <td v-for="cell in row.getVisibleCells()" :key="cell.id">
          <FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
        </td>
      </tr>
    </tbody>
  </table>
</template>
Why must getCoreRowModel() always be provided?
What does FlexRender do?
16. How do you add client-side sorting and column filtering to a TanStack Table in Vue?

Pass getSortedRowModel() and getFilteredRowModel() to useVueTable. Use controlled refs for state so sort/filter can be persisted to the URL or storage.

<script setup lang="ts">
import {
  useVueTable, createColumnHelper,
  getCoreRowModel, getSortedRowModel, getFilteredRowModel,
  type SortingState,
} from "@tanstack/vue-table"
import { ref } from "vue"

type Employee = { name:string; department:string; salary:number }
const data=ref<Employee[]>([/*...*/])
const sorting=ref<SortingState>([])
const globalFilter=ref("")

const ch = createColumnHelper<Employee>()
const columns = [
  ch.accessor("name",{header:"Name",enableSorting:true}),
  ch.accessor("department",{header:"Department",enableSorting:true}),
  ch.accessor("salary",{header:"Salary",enableSorting:true}),
]

const table = useVueTable({
  get data(){ return data.value },
  columns,
  state:{
    get sorting(){ return sorting.value },
    get globalFilter(){ return globalFilter.value },
  },
  onSortingChange:      v=>{ sorting.value=typeof v==="function"?v(sorting.value):v },
  onGlobalFilterChange: v=>{ globalFilter.value=v },
  getCoreRowModel:     getCoreRowModel(),
  getSortedRowModel:   getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
})
</script>
<template>
  <input v-model="globalFilter" placeholder="Search..." />
</template>
Difference between controlled and uncontrolled state in TanStack Table?
What must you add to useVueTable to enable filtering?
17. How do you implement pagination with TanStack Table?

Add getPaginationRowModel() to the table options. The table exposes page-navigation helpers and pagination state. Controlled pagination allows URL sync.

<script setup lang="ts">
import {
  useVueTable, createColumnHelper,
  getCoreRowModel, getPaginationRowModel,
} from "@tanstack/vue-table"
import { ref } from "vue"

const data=ref(largeDataset)
const table=useVueTable({
  get data(){ return data.value },
  columns,
  getCoreRowModel:       getCoreRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  initialState: { pagination:{ pageSize:20, pageIndex:0 } },
})
</script>
<template>
  <tr v-for="row in table.getRowModel().rows" :key="row.id">
    <td v-for="cell in row.getVisibleCells()" :key="cell.id">...</td>
  </tr>
  <div>
    <button @click="table.previousPage()" :disabled="!table.getCanPreviousPage()">Prev</button>
    <span>Page {{ table.getState().pagination.pageIndex+1 }} of {{ table.getPageCount() }}</span>
    <button @click="table.nextPage()" :disabled="!table.getCanNextPage()">Next</button>
    <select :value="table.getState().pagination.pageSize"
            @change="table.setPageSize(Number(($event.target as HTMLSelectElement).value))">
      <option v-for="n in [10,20,50]" :key="n" :value="n">Show {{n}}</option>
    </select>
  </div>
</template>
What does table.getRowModel().rows return when pagination is active?
Difference between initialState.pagination and state.pagination?
18. How do you add column visibility toggling and row selection to a TanStack Table?

Both are built-in. Column visibility lets users show/hide columns; row selection provides a managed selected-rows map for bulk actions.

<script setup lang="ts">
import {
  useVueTable, createColumnHelper, getCoreRowModel,
  type VisibilityState, type RowSelectionState,
} from "@tanstack/vue-table"
import { h, ref, computed } from "vue"

const colVis=ref<VisibilityState>({})
const rowSel=ref<RowSelectionState>({})

const ch=createColumnHelper<Person>()
const columns=[
  ch.display({
    id:"select",
    header:({table})=>h("input",{type:"checkbox",
      checked:table.getIsAllPageRowsSelected(),
      onChange:table.getToggleAllPageRowsSelectedHandler()}),
    cell:({row})=>h("input",{type:"checkbox",
      checked:row.getIsSelected(),
      onChange:row.getToggleSelectedHandler()}),
  }),
  ch.accessor("name",{header:"Name"}),
  ch.accessor("email",{header:"Email"}),
]

const table=useVueTable({
  get data(){ return data.value }, columns,
  state:{
    get columnVisibility(){ return colVis.value },
    get rowSelection(){ return rowSel.value },
  },
  onColumnVisibilityChange: v=>{ colVis.value=typeof v==="function"?v(colVis.value):v },
  onRowSelectionChange:     v=>{ rowSel.value=typeof v==="function"?v(rowSel.value):v },
  getCoreRowModel: getCoreRowModel(),
  enableRowSelection: true,
})
const selected=computed(()=>table.getSelectedRowModel().rows.map(r=>r.original))
</script>
<template>
  <label v-for="col in table.getAllLeafColumns()" :key="col.id">
    <input type="checkbox" :checked="col.getIsVisible()" @change="col.toggleVisibility()" />
    {{col.id}}
  </label>
  <p>{{selected.length}} rows selected</p>
</template>
What does columnHelper.display() do that accessor() does not?
How do you retrieve selected row data?
19. What is TanStack Form and how do you build a basic form in Vue?

TanStack Form is a headless, type-safe form state library with built-in async validation, field arrays and subscriptions. Unlike Vue-specific libraries (vee-validate, Vuelidate) it is framework-agnostic — the same library targets Vue, React and Solid via adapters.

Form Library Comparison
FeatureTanStack Formvee-validateVuelidate
TypeScript inferenceFirst-class — field types inferredGood with macrosPartial
Async validationBuilt-in per-fieldVia Yup / customVia custom
FrameworkAgnostic (adapters)Vue-specificVue-specific
Field arraysBuilt-inVia FieldArray componentManual
<script setup lang="ts">
import { useForm } from "@tanstack/vue-form"

const form = useForm({
  defaultValues: { username:"", email:"", age:0 },
  onSubmit: async ({ value })=>{
    // value fully typed: { username:string, email:string, age:number }
    await createUser(value)
  },
})
</script>
<template>
  <form @submit.prevent="form.handleSubmit()">
    <form.Field name="username">
      <template #default="{ field }">
        <input :value="field.state.value"
               @blur="field.handleBlur"
               @input="field.handleChange(($event.target as HTMLInputElement).value)" />
        <span v-if="field.state.meta.errors.length">
          {{ field.state.meta.errors[0] }}
        </span>
      </template>
    </form.Field>
    <button type="submit" :disabled="form.state.isSubmitting">Save</button>
  </form>
</template>
What does 'headless' mean for TanStack Form?
What does field.state.meta.errors contain?
20. How do you add synchronous and asynchronous field validation in TanStack Form?

Validators run on onChange, onBlur, or onSubmit, synchronously or asynchronously. Async validators support debounce to avoid API calls on every keystroke.

<script setup lang="ts">
import { useForm }       from "@tanstack/vue-form"
import { zodValidator } from "@tanstack/zod-form-adapter"
import { z }            from "zod"

const form=useForm({
  defaultValues:    { username:"", email:"" },
  validatorAdapter: zodValidator(),
  onSubmit: async ({ value })=>{ await register(value) },
})
</script>
<template>
  <form @submit.prevent="form.handleSubmit()">

    <!-- Sync Zod validation -->
    <form.Field name="username"
      :validators="{
        onChange: z.string().min(3,'Min 3 characters'),
        onBlur:   z.string().regex(/^[a-z0-9]+$/,'Lowercase/numbers only'),
      }"
    >
      <template #default="{ field }">
        <input :value="field.state.value" @blur="field.handleBlur"
               @input="field.handleChange(($event.target as HTMLInputElement).value)" />
        <p v-for="e in field.state.meta.errors" :key="e">{{e}}</p>
      </template>
    </form.Field>

    <!-- Async validation with debounce -->
    <form.Field name="email"
      :validators="{
        onChangeAsync: async ({ value })=>{
          const taken=await checkEmailTaken(value)
          return taken ? 'Email already registered' : undefined
        },
        onChangeAsyncDebounceMs: 500,
      }"
    >
      <template #default="{ field }">
        <input :value="field.state.value"
               @input="field.handleChange(($event.target as HTMLInputElement).value)" />
        <span v-if="field.state.meta.isValidating">Checking...</span>
        <p v-for="e in field.state.meta.errors" :key="e">{{e}}</p>
      </template>
    </form.Field>
    <button type="submit">Register</button>
  </form>
</template>
What does onChangeAsyncDebounceMs do?
What should an async validator return when the field is valid?
21. How do you manage dynamic field arrays in TanStack Form?

form.Field with mode='array' provides pushValue, removeValue, and moveValue for dynamic lists such as phone numbers, addresses, or order line items.

<script setup lang="ts">
import { useForm } from "@tanstack/vue-form"
const form=useForm({
  defaultValues: { friends:[{ name:"", age:0 }] },
  onSubmit: ({ value })=>console.log(value),
})
</script>
<template>
  <form @submit.prevent="form.handleSubmit()">
    <form.Field name="friends" mode="array">
      <template #default="ff">
        <div v-for="(_,i) in ff.state.value" :key="i">
          <form.Field :name="`friends[${i}].name`">
            <template #default="{ field }">
              <input :placeholder="`Friend ${i+1} name`"
                     :value="field.state.value"
                     @input="field.handleChange(($event.target as HTMLInputElement).value)" />
            </template>
          </form.Field>
          <button type="button" @click="ff.removeValue(i)">Remove</button>
        </div>
        <button type="button" @click="ff.pushValue({ name:'', age:0 })">Add friend</button>
      </template>
    </form.Field>
    <button type="submit">Save</button>
  </form>
</template>
What does mode='array' on form.Field do?
How do you reference a nested field inside a field array?
22. What is TanStack Virtual and when should you use it?

TanStack Virtual renders only the DOM nodes visible in the viewport. With thousands of rows, rendering everything creates thousands of DOM nodes causing slow renders, janky scroll, and high memory use. TanStack Virtual computes which items are in view and renders only those plus a small overscan buffer.

<script setup lang="ts">
import { useVirtualizer } from "@tanstack/vue-virtual"
import { ref, computed }  from "vue"

const parentRef=ref<HTMLDivElement|null>(null)
const items=Array.from({length:10_000},(_,i)=>({id:i,name:`Item ${i+1}`}))

const virt=useVirtualizer({
  count:            items.length,
  getScrollElement: ()=>parentRef.value,
  estimateSize:     ()=>40,
  overscan:         5,
})
const vRows  = computed(()=>virt.value.getVirtualItems())
const totalH = computed(()=>virt.value.getTotalSize())
</script>
<template>
  <div ref="parentRef" style="height:500px;overflow-y:auto;">
    <div :style="{ height:`${totalH}px`, position:'relative' }">
      <div v-for="vr in vRows" :key="vr.key"
           :style="{ position:'absolute',top:0,
                     transform:`translateY(${vr.start}px)`,
                     height:`${vr.size}px`,width:'100%' }">
        {{ items[vr.index].name }}
      </div>
    </div>
  </div>
</template>
How does TanStack Virtual keep the browser scrollbar correct?
When is TanStack Virtual NOT necessary?
23. How do you handle variable-height rows in TanStack Virtual?

Pass a measureElement callback so TanStack Virtual reads each row's actual rendered height and updates its internal size map. Provide estimateSize for initial layout before items are mounted.

<script setup lang="ts">
import { useVirtualizer } from "@tanstack/vue-virtual"
import { ref, computed }  from "vue"

const parentRef=ref<HTMLDivElement|null>(null)
const messages=ref(largeMessageList)

const virt=useVirtualizer({
  count:            computed(()=>messages.value.length).value,
  getScrollElement: ()=>parentRef.value,
  estimateSize:     ()=>80,
  overscan:         5,
  measureElement:   el=>el?.getBoundingClientRect().height,
})
const vItems = computed(()=>virt.value.getVirtualItems())
const totalH = computed(()=>virt.value.getTotalSize())
</script>
<template>
  <div ref="parentRef" style="height:600px;overflow-y:auto;">
    <div :style="{ height:`${totalH}px`, position:'relative' }">
      <div v-for="vi in vItems" :key="vi.key"
           :ref="node=>virt.measureElement(node)"
           :data-index="vi.index"
           :style="{ position:'absolute',top:0,
                     transform:`translateY(${vi.start}px)`,width:'100%' }">
        <MessageCard :message="messages[vi.index]" />
      </div>
    </div>
  </div>
</template>
What does passing a ref to virtualizer.measureElement do?
Why still provide estimateSize when using measureElement?
24. How do you combine useInfiniteQuery with useVirtualizer for an infinitely scrolling virtualised list?

This is one of the most powerful TanStack patterns: useInfiniteQuery fetches pages as the user scrolls while useVirtualizer renders only visible DOM nodes — together they handle millions of rows with minimal memory.

<script setup lang="ts">
import { ref, computed, watchEffect } from "vue"
import { useInfiniteQuery }           from "@tanstack/vue-query"
import { useVirtualizer }             from "@tanstack/vue-virtual"

const parentRef=ref<HTMLDivElement|null>(null)

const { data, fetchNextPage, hasNextPage, isFetchingNextPage }=useInfiniteQuery({
  queryKey:         ["posts","infinite"],
  queryFn:          ({pageParam})=>fetchPosts({page:pageParam}),
  initialPageParam: 0,
  getNextPageParam: last=>last.nextPage??undefined,
})

const allRows=computed(()=>data.value?data.value.pages.flatMap(p=>p.rows):[])

const virt=useVirtualizer({
  // +1 sentinel row triggers next-page fetch when it enters viewport
  get count(){ return hasNextPage.value?allRows.value.length+1:allRows.value.length },
  getScrollElement: ()=>parentRef.value,
  estimateSize:     ()=>72,
  overscan:         5,
})
const vItems=computed(()=>virt.value.getVirtualItems())

watchEffect(()=>{
  const last=vItems.value.at(-1)
  if(!last) return
  if(last.index>=allRows.value.length-1 && hasNextPage.value && !isFetchingNextPage.value)
    fetchNextPage()
})
</script>
What is the role of the sentinel row (+1 to count)?
Why use watchEffect instead of an onScroll handler?
25. How do you implement optimistic updates with TanStack Query?

Optimistic updates change the UI immediately before the server responds. On error the UI rolls back to the snapshot saved in onMutate.

const { mutate:toggleTodo }=useMutation({
  mutationFn: (todo:Todo)=>
    fetch(`/api/todos/${todo.id}`,{
      method:"PATCH",
      body:JSON.stringify({done:!todo.done}),
    }).then(r=>r.json()),

  onMutate: async (todo)=>{
    // Cancel in-flight fetches so they don't overwrite our update
    await qc.cancelQueries({queryKey:["todos"]})
    const prev=qc.getQueryData<Todo[]>(["todos"])
    qc.setQueryData<Todo[]>(["todos"],
      old=>old?.map(t=>t.id===todo.id?{...t,done:!t.done}:t)??[])
    return { prev }
  },

  onError: (_,__,ctx)=>{
    if(ctx?.prev) qc.setQueryData(["todos"],ctx.prev)
  },

  onSettled: ()=>{ qc.invalidateQueries({queryKey:["todos"]}) },
})
Why call cancelQueries inside onMutate?
What must onMutate return for rollback to work?
26. What are the status and fetchStatus values in TanStack Query v5 and how do they drive UI?

v5 separates state into two orthogonal dimensions: status (what does the cache contain?) and fetchStatus (is a network request happening?). Understanding both prevents wrong loading UI.

Status vs fetchStatus
statusfetchStatusMeaning
pendingfetchingNo data yet, first load — show full-page spinner
pendingidleenabled=false — query paused, will never fetch
successfetchingHas data, refetching in background — show subtle indicator
successidleHas fresh data, nothing happening — steady state
erroridleAll retries exhausted — show error UI
<script setup lang="ts">
import { useQuery } from "@tanstack/vue-query"
const {
  data, isPending, isSuccess, isError,
  isFetching,  // fetchStatus==="fetching"
  isLoading,   // isPending && isFetching — true ONLY on first load
  error,
}=useQuery({ queryKey:["posts"], queryFn:fetchPosts })
</script>
<template>
  <LoadingSpinner v-if="isLoading" />
  <ErrorMessage  v-else-if="isError" :msg="error.message" />
  <div v-else>
    <RefetchBadge v-if="isFetching" />
    <PostList :posts="data" />
  </div>
</template>
Difference between isPending and isLoading in TanStack Query v5?
Which combination describes a background refetch of data you already have?
27. How do you create links and perform programmatic navigation in TanStack Router with Vue?

TanStack Router provides <RouterLink> for declarative links and useNavigate() for programmatic navigation. Both are fully type-safe — TypeScript errors on invalid paths or missing params.

<script setup lang="ts">
import { RouterLink, RouterOutlet, useNavigate } from "@tanstack/vue-router"
const navigate=useNavigate()

async function handleLogin(creds){
  await login(creds)
  navigate({ to:"/dashboard", replace:true })
}

function openUser(id:number){
  navigate({ to:"/users/$userId", params:{userId:String(id)}, search:{tab:"profile"} })
}
</script>
<template>
  <RouterLink to="/home">Home</RouterLink>

  <RouterLink
    v-for="u in users" :key="u.id"
    :to="{ to:'/users/$userId', params:{ userId:String(u.id) } }"
    :active-props="{ class:'font-bold text-blue-600' }"
    :preload="'intent'"
  >{{ u.name }}</RouterLink>

  <RouterOutlet />
</template>
What does preload='intent' on RouterLink do?
What is RouterOutlet's purpose?
28. How does TanStack Router handle route-level errors and loading states?

Each route accepts errorComponent and pendingComponent. When a loader throws, errorComponent renders instead of the page — no flash of protected content, no try/catch in every component.

import { createRoute }       from "@tanstack/vue-router"
import { defineComponent, h } from "vue"

const RouteError=defineComponent({
  props: { error:{ type:Error, required:true } },
  setup(props){
    return ()=>h("div",null,[
      h("h2","Something went wrong"),
      h("p",props.error.message),
    ])
  },
})
const RoutePending=defineComponent({
  setup:()=>()=>h("div","Loading..."),
})

export const userDetailRoute=createRoute({
  getParentRoute: ()=>rootRoute,
  path:           "/users/$userId",
  component:      UserDetailPage,
  pendingComponent: RoutePending,
  pendingMs:        200,   // only show pending if loader > 200 ms — avoids flash
  errorComponent:   RouteError,
  loader: async ({ params })=>{
    const user=await fetchUser(Number(params.userId))
    if(!user) throw new Error("User not found")
    return { user }
  },
})
What does pendingMs do on a TanStack Router route?
What happens when a loader throws in TanStack Router?
29. How do you integrate TanStack Table with server-side sorting, filtering and pagination?

Set manualSorting and manualPagination to true. TanStack Table manages UI state (current sort, page) but does not process rows — you read that state and send it to your API.

<script setup lang="ts">
import { ref, computed }  from "vue"
import { useQuery, keepPreviousData } from "@tanstack/vue-query"
import { useVueTable, createColumnHelper, getCoreRowModel,
         type SortingState, type PaginationState } from "@tanstack/vue-table"

const sorting    = ref<SortingState>([])
const pagination = ref<PaginationState>({ pageIndex:0, pageSize:20 })

const { data:serverData } = useQuery({
  queryKey: computed(()=>["users",{ sort:sorting.value[0], ...pagination.value }]),
  queryFn:  ()=>fetchUsers({
    sortBy:   sorting.value[0]?.id,
    sortDir:  sorting.value[0]?.desc?"desc":"asc",
    page:     pagination.value.pageIndex,
    pageSize: pagination.value.pageSize,
  }),
  placeholderData: keepPreviousData,
})

const ch=createColumnHelper<User>()
const columns=[ch.accessor("name",{header:"Name",enableSorting:true})]

const table=useVueTable({
  get data(){    return serverData.value?.rows??[] },
  get rowCount(){ return serverData.value?.total??0 },
  columns,
  manualSorting:    true,
  manualPagination: true,
  state:{
    get sorting(){    return sorting.value },
    get pagination(){ return pagination.value },
  },
  onSortingChange:    v=>{ sorting.value=typeof v==="function"?v(sorting.value):v },
  onPaginationChange: v=>{ pagination.value=typeof v==="function"?v(pagination.value):v },
  getCoreRowModel: getCoreRowModel(),
})
</script>
What does manualSorting:true tell TanStack Table?
Why is keepPreviousData important with server-side pagination?
30. What developer tools does TanStack provide and how do you add them in Vue?

TanStack Query and TanStack Router each ship a devtools panel. Query devtools visualise the cache, let you manually invalidate/refetch, and show observer counts. Router devtools show the matched route tree, params and search params in real time. Both are tree-shaken in production.

<!-- @tanstack/vue-query-devtools -->
<script setup lang="ts">
import { VueQueryDevtools } from "@tanstack/vue-query-devtools"
</script>
<template>
  <RouterView />
  <!-- Automatically excluded from production build -->
  <VueQueryDevtools :initial-is-open="false" position="bottom-right" />
</template>

<!-- @tanstack/vue-router-devtools -->
<script setup lang="ts">
import { TanStackRouterDevtools } from "@tanstack/vue-router-devtools"
</script>
<template>
  <RouterOutlet />
  <TanStackRouterDevtools :router="router" position="bottom-left" />
</template>
Do TanStack devtools add code to a production build?
What does TanStack Query devtools show per cached query?
31. What are query key factories and why are they recommended for large Vue apps?

A query key factory centralises all key definitions in one file. This makes invalidations predictable, lets TypeScript catch typos at compile time, and means a renaming/restructuring only needs to happen in one place.

// queryKeys.ts
export const userKeys={
  all:     ()              => ["users"]            as const,
  lists:   ()              => ["users","list"]      as const,
  list:    (f:UserFilter)  => ["users","list",f]    as const,
  details: ()              => ["users","detail"]    as const,
  detail:  (id:number)     => ["users","detail",id] as const,
}

// Component
import { userKeys } from "@/queryKeys"
const { data:user }=useQuery({
  queryKey: userKeys.detail(userId.value),
  queryFn:  ()=>fetchUser(userId.value),
})

// Mutation onSuccess — invalidate ALL user queries
const { mutate:deleteUser }=useMutation({
  mutationFn: (id:number)=>fetch(`/api/users/${id}`,{method:"DELETE"}),
  onSuccess: ()=>{ qc.invalidateQueries({queryKey:userKeys.all()}) },
})

// TypeScript catches typos:
// userKeys.detial(1)         // TS Error: Property 'detial' does not exist
// ["users","detial",1]       // No error — typo silently creates wrong key
Main benefit of a query key factory over inline arrays?
Why use 'as const' on arrays returned by key factories?
32. How do you implement cross-field (form-level) validation and handle submission in TanStack Form?

Form-level validators receive the entire form value, enabling rules like 'password must match confirmPassword'. The onSubmit callback exposes formApi to reset or set field errors programmatically after a server response.

<script setup lang="ts">
import { useForm } from "@tanstack/vue-form"

const form=useForm({
  defaultValues: { password:"", confirm:"" },
  validators:{
    onSubmit: ({ value })=>{
      if(value.password!==value.confirm) return "Passwords do not match"
      return undefined
    },
  },
  onSubmit: async ({ value, formApi })=>{
    try{
      await registerUser(value)
      formApi.reset()
    } catch(err){
      formApi.setFieldMeta("confirm", meta=>({...meta, errors:["Server error: try again"]}))
    }
  },
})
</script>
<template>
  <form @submit.prevent="form.handleSubmit()">
    <p v-if="form.state.errors.length" class="form-error">
      {{ form.state.errors[0] }}
    </p>
    <!-- ...fields... -->
    <button type="submit" :disabled="form.state.isSubmitting">Register</button>
  </form>
</template>
Difference between field-level and form-level validators?
What does form.state.isSubmitting track?
33. How do you pass variables to a mutation and use them across lifecycle callbacks?

The mutationFn, onMutate, onSuccess, onError, and onSettled callbacks all receive the mutation variables as their first argument. The exposed variables ref also holds the in-flight variables for use in the template.

<script setup lang="ts">
import { useMutation, useQueryClient } from "@tanstack/vue-query"
const qc=useQueryClient()

interface UpdateInput { id:number; name:string; email:string }

const { mutate:updateUser, isPending, variables }=useMutation({
  mutationFn: (input:UpdateInput)=>
    fetch(`/api/users/${input.id}`,{
      method:"PUT",
      body:JSON.stringify(input),
    }).then(r=>r.json()),

  onSuccess: (data, vars)=>{
    qc.setQueryData(["user",vars.id], data)  // write response directly into cache
    toast.success(`Updated ${vars.name}`)
  },

  onError: (err, vars)=>{
    toast.error(`Failed to update ${vars.name}: ${err.message}`)
  },
})
</script>
<template>
  <button @click="updateUser(form)" :disabled="isPending">
    {{ isPending ? `Saving ${variables?.name}...` : "Save" }}
  </button>
</template>
What does the 'variables' ref exposed by useMutation contain?
When is setQueryData preferred over invalidateQueries after a mutation?
34. How do you virtualise a horizontal list or two-dimensional grid with TanStack Virtual?

TanStack Virtual supports vertical (default), horizontal (horizontal:true), and grid layouts using two virtualizers — one per axis. Only the cells whose row AND column are both in view are rendered.

<script setup lang="ts">
import { useVirtualizer } from "@tanstack/vue-virtual"
import { ref, computed }  from "vue"

const parentRef=ref<HTMLDivElement|null>(null)
const ROW_COUNT=1000, COL_COUNT=50

const rowVirt=useVirtualizer({
  count:            ROW_COUNT,
  getScrollElement: ()=>parentRef.value,
  estimateSize:     ()=>35,
  overscan:         5,
})
const colVirt=useVirtualizer({
  horizontal:       true,
  count:            COL_COUNT,
  getScrollElement: ()=>parentRef.value,
  estimateSize:     ()=>120,
  overscan:         5,
})

const vRows=computed(()=>rowVirt.value.getVirtualItems())
const vCols=computed(()=>colVirt.value.getVirtualItems())
const totW =computed(()=>colVirt.value.getTotalSize())
const totH =computed(()=>rowVirt.value.getTotalSize())
</script>
<template>
  <div ref="parentRef" style="height:400px;width:800px;overflow:auto;">
    <div :style="{ height:`${totH}px`, width:`${totW}px`, position:'relative' }">
      <div v-for="vr in vRows" :key="vr.key"
           :style="{ position:'absolute',top:0,
                     transform:`translateY(${vr.start}px)`,width:'100%' }">
        <div v-for="vc in vCols" :key="vc.key"
             :style="{ position:'absolute',left:0,
                       transform:`translateX(${vc.start}px)`,
                       width:`${vc.size}px`,height:`${vr.size}px` }">
          Cell ({{vr.index}},{{vc.index}})
        </div>
      </div>
    </div>
  </div>
</template>
How do you enable horizontal virtualisation in TanStack Virtual?
In a 2D virtualised grid, how many cells are in the DOM at any time?
35. What are the most common TanStack Query mistakes in Vue and how do you avoid them?

Knowing what not to do is as important as knowing the API. These patterns trip up most developers new to TanStack Query in Vue.

Common Mistakes
MistakeProblemFix
queryKey not including all query variablesStale data served when variable changesAdd every queryFn variable to the key
Non-reactive queryKey in VueQuery doesn't re-run when props changeWrap queryKey in computed() when it reads reactive state
Server state in both TanStack Query and PiniaTwo sources of truth — sync bugsUse TanStack Query as the single source for server data
New QueryClient per componentEach component gets its own empty cacheCreate QueryClient once in main.ts
staleTime=0 for slow/static dataUnnecessary refetch on every window focusSet staleTime to match data update frequency
// WRONG: non-reactive key — captured once at setup, never updates
const { data }=useQuery({
  queryKey: ["user", props.userId],  // plain read, not reactive
  queryFn:  ()=>fetchUser(props.userId),
})

// CORRECT: reactive via computed()
const { data }=useQuery({
  queryKey: computed(()=>["user", props.userId]),
  queryFn:  ()=>fetchUser(props.userId),
})

// WRONG: duplicating server state into Pinia
const { data:user }=useQuery({
  queryKey: ["user",1],
  queryFn:  fetchUser,
  onSuccess: u=>userStore.setUser(u), // unnecessary sync — two sources of truth
})
// CORRECT: read data directly from useQuery wherever needed
Why must queryKey be wrapped in computed() in Vue when it depends on a prop?
What is the problem with keeping server state in both TanStack Query and Pinia?
36. How do you implement protected routes and auth redirects in TanStack Router?

Use beforeLoad with throw redirect(). This halts the route loading process before the component ever mounts — no flash of protected content.

import { createRoute, redirect } from "@tanstack/vue-router"
import { getAuthToken }          from "../auth"

async function requireAuth({ location }:any){
  if(!getAuthToken())
    throw redirect({ to:"/login", search:{ redirect:location.href } })
}

export const dashboardRoute=createRoute({
  getParentRoute: ()=>rootRoute,
  path:           "/dashboard",
  component:      DashboardPage,
  beforeLoad:     requireAuth,
})

export const loginRoute=createRoute({
  getParentRoute: ()=>rootRoute,
  path:           "/login",
  component:      LoginPage,
  beforeLoad: ({ search })=>{
    if(getAuthToken())
      throw redirect({ to:(search as any).redirect??"/dashboard", replace:true })
  },
})

// In LoginPage.vue
import { useSearch, useNavigate } from "@tanstack/vue-router"
const search=useSearch({ from:"/login" })
const nav=useNavigate()
async function handleLogin(creds){
  await login(creds)
  nav({ to:(search as any).redirect??"/dashboard", replace:true })
}
Why is throw redirect() in beforeLoad preferred over a conditional redirect component?
Why pass the current URL as a search param on the auth redirect?
37. How do you render custom Vue components inside TanStack Table cells?

The cell column definition accepts a function returning a string, VNode, or component. FlexRender handles all three transparently so you never need conditional render logic.

<script setup lang="ts">
import { h, defineComponent, computed } from "vue"
import {
  useVueTable, createColumnHelper, getCoreRowModel, FlexRender,
} from "@tanstack/vue-table"

type Order={ id:number; status:string; amount:number }

const StatusBadge=defineComponent({
  props:{ status:String },
  setup(props){
    const colour=computed(()=>({
      paid:"bg-green-100 text-green-800",
      pending:"bg-yellow-100 text-yellow-800",
      cancelled:"bg-red-100 text-red-800",
    }[props.status??"pending"]??"bg-gray-100"))
    return ()=>h("span",{class:colour.value},props.status)
  },
})

const ch=createColumnHelper<Order>()
const columns=[
  ch.accessor("status",{
    header:"Status",
    cell: info=>h(StatusBadge,{status:info.getValue()}),
  }),
  ch.accessor("amount",{
    header:"Amount",
    cell: info=>`$${info.getValue().toFixed(2)}`,
  }),
  ch.display({
    id:"actions",
    header:"Actions",
    cell: ({row})=>h("div",null,[
      h("button",{onClick:()=>editOrder(row.original)},"Edit"),
      h("button",{onClick:()=>deleteOrder(row.original.id)},"Delete"),
    ]),
  }),
]
</script>
What does info.getValue() return in a TanStack Table cell function?
Why use columnHelper.display() for an actions column?
38. How do the TanStack libraries compose into a full application stack, and what is TanStack Start?

Each library is independently useful, but together they form a coherent, type-safe, headless application stack — no additional state management or routing library required.

Full TanStack Stack
LayerLibraryResponsibility
RoutingTanStack RouterType-safe navigation, search params, loaders
Server stateTanStack QueryFetching, caching, mutations, background sync
Form stateTanStack FormForm lifecycle, validation, field arrays
Table / data UITanStack TableSorting, filtering, pagination, selection
Large listsTanStack VirtualRender only visible rows/columns
// Typical page: Router search params → Query key → Table state
<script setup lang="ts">
import { computed }               from "vue"
import { useSearch, useNavigate } from "@tanstack/vue-router"
import { useQuery }               from "@tanstack/vue-query"
import { useVueTable, getCoreRowModel, createColumnHelper } from "@tanstack/vue-table"

// 1. URL state via Router
const search=useSearch({ from:"/users" })

// 2. Server data via Query (key mirrors URL state)
const { data }=useQuery({
  queryKey: computed(()=>["users",{page:search.page,q:search.q}]),
  queryFn:  ()=>fetchUsers({page:search.page,q:search.q}),
})

// 3. Table UI logic via Table
const ch=createColumnHelper<User>()
const table=useVueTable({
  get data(){ return data.value?.users??[] },
  columns:[ch.accessor("name",{header:"Name"})],
  manualPagination:true,
  getCoreRowModel:getCoreRowModel(),
})
</script>
// TanStack Start (2024-2025):
// Full-stack framework built on TanStack Router adding SSR,
// server functions, streaming, and file-based routing.
// Stable for React; Vue support in progress.
Main architectural benefit of driving TanStack Query's queryKey from Router search params?
What does TanStack Start add on top of TanStack Router?
«
»
DataStructures

Comments & Discussions