TanStack Family Starter Guide: Adding It to Your React 18+ and Vue 3 Projects

by JeariCk 11 min read
tanstack: The open-source application stack for the web

TanStack is a set of headless utility libraries covering data fetching, routing, and tables. In React 18+ and Vue 3 projects, a few lines of code gets you data requests, page navigation, and data tables — all type-safe.

tanstack: The open-source application stack for the web
tanstack: The open-source application stack for the web

What is TanStack?

TanStack is an open-source collection of frontend tools created by Tanner Linsley. If you’ve used React Query or React Table before, you’ve already met their predecessors — they’ve now been rebranded under TanStack and support Vue, Solid, Svelte, and more.

Back when he was building React Table v7, I doubt he expected it to grow into a whole ecosystem. V8 dropped in mid-2022 with a full TypeScript rewrite and support beyond React. By 2025-2026, people are comparing TanStack Start (their full-stack React framework) with Next.js and Remix.

Three libraries we’re covering today:

LibraryFormer NameWhat It Solves
TanStack QueryReact Query / Vue QueryData fetching, caching, state sync
TanStack RouterType-safe routing (React & Solid only)
TanStack TableReact TableHeadless table engine, multi-framework

Why these? Most frameworks don’t ship a complete data-fetching solution. Developers end up writing mountains of `useEffect` + `fetch` boilerplate, or misusing Redux/Pinia as a cache. TanStack Query kills all that repetition. Router and Table follow the same philosophy — “here’s your engine, you build the body.”


Prerequisites

React 18+ Project
```bash

# Existing project

npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table

# New project with Vite

npm create vite@latest my-app -- --template react-ts

cd my-app

npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table

```

Requirements: React 18+ (with createRoot), TypeScript 5.3+ (recommended but not required).

Vue 3 Project
```bash

# Existing project

npm install @tanstack/vue-query @tanstack/vue-table

# New project

npm create vite@latest my-app -- --template vue-ts

cd my-app

npm install @tanstack/vue-query @tanstack/vue-table

```

Note: Router only supports React and Solid for now — Vue is still waiting in line. Vue projects should stick with Vue Router for navigation, but Query and Table work just as well.


TanStack Query: Your Data Fetching Savior

In React 18+

You need a `QueryClient` wrapping your whole app. Think of it like Redux’s Provider, but for data fetching only.

```tsx

// main.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

import { createRoot } from 'react-dom/client'

import App from './App'

const queryClient = new QueryClient()

createRoot(document.getElementById('root')!).render(

  <QueryClientProvider client={queryClient}>

    <App />

  </QueryClientProvider>

)

```

QueryClient’s defaults are already pretty smart: automatic request deduplication, caching, background refetch, refetch on window focus — all works out of the box.

A component with `useQuery`:

```tsx

// components/UserList.tsx

import { useQuery } from '@tanstack/react-query'

type User = {

  id: number

  name: string

  email: string

}

const fetchUsers = async (): Promise<User[]> => {

  const res = await fetch('https://jsonplaceholder.typicode.com/users')

  if (!res.ok) throw new Error('Failed to fetch')

  return res.json()

}

export default function UserList() {

  const { data, isLoading, error } = useQuery({

    queryKey: ['users'],

    queryFn: fetchUsers,

  })

  if (isLoading) return <div>Loading...</div>

  if (error) return <div>Error: {error.message}</div>

  return (

    <ul>

      {data?.map(user => (

        <li key={user.id}>{user.name} — {user.email}</li>

      ))}

    </ul>

  )

}

```

Three core concepts:

1. Queries — `useQuery` fetches data with built-in caching

2. Mutations — `useMutation` creates, updates, or deletes data

3. Query Invalidation — tells queries to re-fetch after data changes

Putting it together:

```tsx

// components/TodoApp.tsx

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

type Todo = { id: number; title: string; completed: boolean }

const fetchTodos = async (): Promise<Todo[]> => {

  const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')

  return res.json()

}

const addTodo = async (title: string) => {

  const res = await fetch('https://jsonplaceholder.typicode.com/todos', {

    method: 'POST',

    body: JSON.stringify({ title, completed: false }),

    headers: { 'Content-Type': 'application/json' },

  })

  return res.json()

}

export default function TodoApp() {

  const queryClient = useQueryClient()

  const { data: todos, isLoading } = useQuery({

    queryKey: ['todos'],

    queryFn: fetchTodos,

  })

  const mutation = useMutation({

    mutationFn: (title: string) => addTodo(title),

    onSuccess: () => {

      queryClient.invalidateQueries({ queryKey: ['todos'] })

    },

  })

  if (isLoading) return <div>Loading...</div>

  return (

    <div>

      <button onClick={() => mutation.mutate('Buy groceries')}>

        {mutation.isPending ? 'Adding...' : 'Add Todo'}

      </button>

      <ul>

        {todos?.map(todo => (

          <li key={todo.id}>

            {todo.title} {todo.completed ? '✅' : '⬜'}

          </li>

        ))}

      </ul>

    </div>

  )

}

```

Key takeaways:

– `queryKey: [‘todos’]` is the cache identifier, globally unique

– `useQuery` handles loading, error, and data states automatically

– `useMutation` + `invalidateQueries` is the standard “mutate → refresh” pattern

– No more manual `useEffect` + `useState`

A page that used to require 30 lines of data-fetching boilerplate now takes just a few.

In Vue 3

Same idea, hooks become composables:

```bash

npm install @tanstack/vue-query

```

Plugin setup:

```ts

// main.ts

import { createApp } from 'vue'

import { VueQueryPlugin } from '@tanstack/vue-query'

import App from './App.vue'

createApp(App).use(VueQueryPlugin)

```

Use in a component:

```vue

<!-- components/UserList.vue -->

<template>

  <div v-if="isLoading">Loading...</div>

  <div v-else-if="error">Error: {{ error.message }}</div>

  <ul v-else>

    <li v-for="user in data" :key="user.id">

      {{ user.name }} — {{ user.email }}

    </li>

  </ul>

</template>

<script setup lang="ts">

import { useQuery } from '@tanstack/vue-query'

type User = { id: number; name: string; email: string }

const fetchUsers = async (): Promise<User[]> => {

  const res = await fetch('https://jsonplaceholder.typicode.com/users')

  if (!res.ok) throw new Error('Failed to fetch')

  return res.json()

}

const { data, isLoading, error } = useQuery({

  queryKey: ['users'],

  queryFn: fetchUsers,

})

</script>

```

Mutation with `useMutation`:

```vue

<!-- components/TodoApp.vue -->

<template>

  <button @click="handleAdd" :disabled="mutation.isPending.value">

    {{ mutation.isPending.value ? 'Adding...' : 'Add Todo' }}

  </button>

  <ul>

    <li v-for="todo in data" :key="todo.id">

      {{ todo.title }} {{ todo.completed ? '✅' : '⬜' }}

    </li>

  </ul>

</template>

<script setup lang="ts">

import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'

const queryClient = useQueryClient()

type Todo = { id: number; title: string; completed: boolean }

const { data } = useQuery({

  queryKey: ['todos'],

  queryFn: async (): Promise<Todo[]> => {

    const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')

    return res.json()

  },

})

const mutation = useMutation({

  mutationFn: (title: string) =>

    fetch('https://jsonplaceholder.typicode.com/todos', {

      method: 'POST',

      body: JSON.stringify({ title, completed: false }),

      headers: { 'Content-Type': 'application/json' },

    }).then(r => r.json()),

  onSuccess: () => {

    queryClient.invalidateQueries({ queryKey: ['todos'] })

  },

})

const handleAdd = () => {

  mutation.mutate('Buy groceries')

}

</script>

```

Honestly, Vue Query’s API is nearly identical to React Query’s. Switching from React to Vue requires zero mental overhead. The only difference — Vue’s reactivity system automatically tracks data changes, so you just bind directly in the template.


TanStack Router: Type-Safe Modern Routing

Router only supports React and Solid for now. Vue adapters are still in the queue, so stick with Vue Router for your Vue projects.

Its biggest selling point is type safety — route params, search params, and state all have type inference built in. No more `useParams()` returning `string | undefined` like react-router-dom, forcing you to assert types manually.

Install:

```bash

npm install @tanstack/react-router

```

Supports both file-based and code-based routing. Let’s start with code-based:

```tsx

// src/router.tsx

import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router'

import App from './App'

import Home from './pages/Home'

import UserDetail from './pages/UserDetail'

const rootRoute = createRootRoute({

  component: App,

})

const homeRoute = createRoute({

  getParentRoute: () => rootRoute,

  path: '/',

  component: Home,

})

const userRoute = createRoute({

  getParentRoute: () => rootRoute,

  path: '/users/$userId',

  component: UserDetail,

})

const routeTree = rootRoute.addChildren([homeRoute, userRoute])

const router = createRouter({ routeTree })

export default router

```

Usage in entry:

```tsx

// main.tsx

import { RouterProvider } from '@tanstack/react-router'

import router from './router'

createRoot(document.getElementById('root')!).render(

  <RouterProvider router={router} />

)

```

In `UserDetail`, params are auto-inferred:

```tsx

// pages/UserDetail.tsx

import { useParams } from '@tanstack/react-router'

export default function UserDetail() {

  const { userId } = useParams({ from: '/users/$userId' })

  return <div>User ID: {userId}</div>

}

```

What’s the difference from react-router-dom? You don’t manually declare param types. If you’ve ever maintained routing with more than three levels of nesting, you know the pain of changing route params and chasing type declarations everywhere.

Search params are also type-safe:

```tsx

import { useSearch } from '@tanstack/react-router'

const search = useSearch({ from: '/users' })

// search.page type is already defined at route definition

```


TanStack Table: Universal Data Table Engine

TanStack Table goes the “headless” route. No pre-built styles or tag structures — it gives you hooks/composables, and you control the HTML and CSS. That means you can build anything from a simple list to an enterprise data grid.

In React 18+
```bash

npm install @tanstack/react-table

```

Create a simple table:

```tsx

// components/BasicTable.tsx

import {

  createColumnHelper,

  flexRender,

  getCoreRowModel,

  useReactTable,

} from '@tanstack/react-table'

type Person = {

  name: string

  age: number

  status: 'Active' | 'Inactive' | 'Pending'

}

const data: Person[] = [

  { name: 'Alice', age: 28, status: 'Active' },

  { name: 'Bob', age: 35, status: 'Inactive' },

  { name: 'Charlie', age: 42, status: 'Pending' },

]

const columnHelper = createColumnHelper<Person>()

const columns = [

  columnHelper.accessor('name', {

    header: 'Name',

    cell: info => info.getValue(),

  }),

  columnHelper.accessor('age', {

    header: 'Age',

    cell: info => info.getValue(),

  }),

  columnHelper.accessor('status', {

    header: 'Status',

    cell: info => {

      const status = info.getValue()

      const color = status === 'Active' ? 'green' : status === 'Inactive' ? 'gray' : 'orange'

      return <span style={{ color }}>{status}</span>

    },

  }),

]

export default function BasicTable() {

  const table = useReactTable({

    data,

    columns,

    getCoreRowModel: getCoreRowModel(),

  })

  return (

    <table border={1}>

      <thead>

        {table.getHeaderGroups().map(headerGroup => (

          <tr key={headerGroup.id}>

            {headerGroup.headers.map(header => (

              <th key={header.id}>

                {flexRender(header.column.columnDef.header, header.getContext())}

              </th>

            ))}

          </tr>

        ))}

      </thead>

      <tbody>

        {table.getRowModel().rows.map(row => (

          <tr key={row.id}>

            {row.getVisibleCells().map(cell => (

              <td key={cell.id}>

                {flexRender(cell.column.columnDef.cell, cell.getContext())}

              </td>

            ))}

          </tr>

        ))}

      </tbody>

    </table>

  )

}

```

Adding sorting and pagination:

```tsx

import {

  getSortedRowModel,

  getPaginationRowModel,

} from '@tanstack/react-table'

import { useState } from 'react'

export default function SortableTable() {

  const [sorting, setSorting] = useState([])

  const table = useReactTable({

    data,

    columns,

    getCoreRowModel: getCoreRowModel(),

    getSortedRowModel: getSortedRowModel(),

    getPaginationRowModel: getPaginationRowModel(),

    state: { sorting },

    onSortingChange: setSorting,

  })

  return (

    <div>

      <table>{/* same rendering as above */}</table>

      <div>

        <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>

          Previous

        </button>

        <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>

          Next

        </button>

      </div>

    </div>

  )

}

```

Hidden bonus: `useReactTable`’s `state` is fully controllable. You can put table state into Redux/Zustand or URL search params for “restore table state on page refresh.”

In Vue 3
```bash

npm install @tanstack/vue-table

```
```vue

<!-- components/BasicTable.vue -->

<template>

  <table border="1">

    <thead>

      <tr v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">

        <th v-for="header in headerGroup.headers" :key="header.id">

          <FlexRender :render="header.column.columnDef.header" :props="header.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>

<script setup lang="ts">

import {

  createColumnHelper,

  getCoreRowModel,

  useVueTable,

  FlexRender,

} from '@tanstack/vue-table'

type Person = { name: string; age: number; status: string }

const data: Person[] = [

  { name: 'Alice', age: 28, status: 'Active' },

  { name: 'Bob', age: 35, status: 'Inactive' },

  { name: 'Charlie', age: 42, status: 'Pending' },

]

const columnHelper = createColumnHelper<Person>()

const columns = [

  columnHelper.accessor('name', { header: 'Name' }),

  columnHelper.accessor('age', { header: 'Age' }),

  columnHelper.accessor('status', { header: 'Status' }),

]

const table = useVueTable({

  data,

  columns,

  getCoreRowModel: getCoreRowModel(),

})

</script>

```

React uses `useReactTable` + `flexRender()` function; Vue uses `useVueTable` + `<FlexRender>` component. Small adapter-level difference, same logic underneath.


Quick Reference

React 18+ Project
```bash

npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table

```

LibraryKey APIsOne-liner
QueryQueryClientProvider + useQuery + useMutationReplaces all useEffect + fetch data management
RoutercreateRouter + RouterProvider + useParamsType-safe routing, auto-inferred params
TableuseReactTable + createColumnHelperHeadless table, any markup you want
Vue 3 Project
```bash

npm install @tanstack/vue-query @tanstack/vue-table

```

LibraryKey APIsOne-liner
QueryVueQueryPlugin + useQuery + useMutationReplaces Pinia for async data management
TableuseVueTable + createColumnHelper + <FlexRender>Headless table, native Vue 3 integration
RouterStick with Vue Router for now

Choosing TanStack means choosing decoupling — Query separates your data layer from UI, Table gives you full control over rendering, and Router locks down routing state through the type system. Three tools, one shared philosophy: “Here’s your engine — you build the body yourself.”

In a frontend ecosystem that keeps reinventing itself every 18 months, this toolkit might be one of the few bets worth placing long-term.

*References:*

– [TanStack Query Official Docs]

– [TanStack Router Official Docs]

– [TanStack Table Official Docs]

📖 Recommended Reading

Take a look at these articles, you may be interested

RSC(React Server Components) Guide — How They Work, Code Examples, and Framework Comparison

Introductory Analysis: Why Next.js 15 Became the Standard for Agentic AI Apps

Web Components + Astro + htmx: The 2026 Lightweight Frontend Trio

React Compiler 1.0 Is Here: Can We Finally Delete useMemo and useCallback?

DigitalOcean Referral Badge

This site is powered by DigitalOcean — reliable cloud hosting for developers.

Leave a Reply

Your email address will not be published. Required fields are marked *