TanStack Cheat Sheet πŸ₯ž

  • Package Directory:
    • Query πŸ€–:
      • React NPM: @tanstack/react-query
      • Devtools NPM: @tanstack/react-query-devtools
    • Router 🧭:
      • React NPM: @tanstack/react-router
      • Devtools NPM: @tanstack/router-devtools
    • Table 🧾:
      • React NPM: @tanstack/react-table
    • Virtual πŸš€:
      • React NPM: @tanstack/react-virtual

πŸ€– TanStack Query (React Query)

  • Mental Model:
    • Query: Cached async result for a queryKey.
    • Mutation: Write action; usually followed by cache update and/or invalidation.
    • staleTime: How long data is considered fresh.
    • gcTime: How long inactive queries stay in cache before garbage collection.
  • Defaults & Gotchas:
    • Inactive queries: Garbage-collected after 5 min by default (tune via gcTime).
    • Failed queries: Retry 3 times by default (tune via retry/retryDelay).
    • Structural sharing: Enabled by default for JSON-compatible data to maintain memo stability.
  • Client & Devtools Setup:
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
     
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: { staleTime: 60000, gcTime: 1800000, retry: 2, refetchOnWindowFocus: true },
        mutations: { retry: 0 }
      }
    })
     
    export function AppProviders({ children }: { children: React.ReactNode }) {
      return (
        <QueryClientProvider client={queryClient}>
          {children}
          <ReactQueryDevtools initialIsOpen={false} />
        </QueryClientProvider>
      )
    }
  • Query Keys:
    • Rules: Always use arrays; entities first, then serializable parameters; keep stable.
    • Factory pattern:
      export const qk = {
        posts: () => ['posts'] as const,
        post: (id: string) => ['posts', id] as const,
        postComments: (id: string) => ['posts', id, 'comments'] as const,
        searchPosts: (q: string, page: number) => ['posts', 'search', { q, page }] as const,
      }
  • Query & Mutation Basics:
    • Options: enabled (dependent queries), select (derive/memoize data), placeholderData (previous page data during transition), refetchOnReconnect, refetchInterval.
    • Usage:
      import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
       
      function Posts() {
        const { data, isPending, error } = useQuery({
          queryKey: qk.posts(),
          queryFn: () => fetch('/api/posts').then(r => r.json()),
        })
        if (isPending) return <div>Loading…</div>
        if (error) return <div>Error: {error.message}</div>
        return data.map((p: any) => <div key={p.id}>{p.title}</div>)
      }
       
      function NewPost() {
        const queryClient = useQueryClient()
        const create = useMutation({
          mutationFn: (body: { title: string }) =>
            fetch('/api/posts', {
              method: 'POST',
              headers: { 'content-type': 'application/json' },
              body: JSON.stringify(body),
            }).then(r => r.json()),
          onSuccess: () => queryClient.invalidateQueries({ queryKey: qk.posts() }),
        })
        return <button onClick={() => create.mutate({ title: 'Hello' })}>Create</button>
      }
  • Cache Actions:
    • Invalidate (marks stale, refetches active): queryClient.invalidateQueries({ queryKey: qk.posts() })
    • Refetch (immediate run): queryClient.refetchQueries({ queryKey: qk.posts(), type: β€˜active’ })
    • Prefetch (instant navigation): queryClient.prefetchQuery({ queryKey: qk.post(id), queryFn: fetchFn, staleTime: 300000 })
    • Cache update: Direct update on queryClient.setQueryData when server result is known or invalidation is too expensive.
  • Optimistic Updates:
    const mutation = useMutation({
      mutationFn: updateTodo,
      onMutate: async (newTodo) => {
        await queryClient.cancelQueries({ queryKey: qk.post(newTodo.id) })
        const previous = queryClient.getQueryData(qk.post(newTodo.id))
        queryClient.setQueryData(qk.post(newTodo.id), newTodo)
        return { previous }
      },
      onError: (_err, newTodo, ctx) => {
        queryClient.setQueryData(qk.post(newTodo.id), ctx?.previous)
      },
      onSettled: (_data, _err, newTodo) => {
        queryClient.invalidateQueries({ queryKey: qk.post(newTodo.id) })
      },
    })
  • Pagination & Infinite Queries:
    • Pagination: Include page in queryKey, use placeholderData: (prev) => prev.
    • Infinite: Include cursor/pageParam in queryKey, implement getNextPageParam.
  • Testing Tips:
    • Fresh client: Create a new QueryClient per test.
    • Determinism: Disable retries (retry: false).
    const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })

🧭 TanStack Router

  • Capabilities: Fully type-safe routing (params and search params), nested layouts via Outlet, loaders + beforeLoad for data/guards, Query integration.
  • Router Cache Defaults:
    • staleTime: Defaults to 0 (routes considered stale, will reload on rematch).
    • gcTime: Defaults to 30 min.
    • preloadStaleTime: Preloaded routes treated as fresh for 30 sec.
    • router.invalidate(): Forces active loaders to reload.
  • Setup Pattern:
    import { createRouter, RouterProvider } from '@tanstack/react-router'
    import { TanStackRouterDevtools } from '@tanstack/router-devtools'
    import { routeTree } from './routeTree.gen'
     
    export type RouterContext = { queryClient: QueryClient; auth: { getUser: () => Promise<any> } }
    export const router = createRouter({ routeTree, context: { queryClient, auth } as RouterContext })
     
    export function App() {
      return (
        <>
          <RouterProvider router={router} />
          <TanStackRouterDevtools position="bottom-right" />
        </>
      )
    }
  • Search Params:
    import { z } from 'zod'
    import { zodValidator, fallback } from '@tanstack/zod-adapter'
     
    const searchSchema = z.object({
      q: fallback(z.string(), ''),
      page: fallback(z.number().int().min(1), 1),
    })
     
    export const Route = createFileRoute('/posts')({
      validateSearch: zodValidator(searchSchema),
    })
  • Loader Integration (Preferred Query Flow):
    • LoaderDeps: Map inputs that trigger loader execution.
    • ensureQueryData: Call inside loader to load cache, read with useSuspenseQuery in component.
    import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
     
    const postsQuery = queryOptions({
      queryKey: qk.posts(),
      queryFn: () => fetch('/api/posts').then(r => r.json()),
    })
     
    export const Route = createFileRoute('/posts')({
      loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery),
      component: Posts,
    })
     
    function Posts() {
      const { data } = useSuspenseQuery(postsQuery)
      return data.map((p: any) => <div key={p.id}>{p.title}</div>)
    }
  • Guards & Redirects:
    • beforeLoad: Use for auth checks, redirecting, and enriching route context.

🧾 TanStack Table

  • Mental Model: Headless architecture; provides row models, state, and helpers. Bring your own markup and styles.
  • Setup:
    import { flexRender, getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table'
     
    type Person = { firstName: string; lastName: string; age: number }
    const columns: ColumnDef<Person>[] = [
      { accessorKey: 'firstName', header: 'First Name' },
      { accessorKey: 'lastName', header: 'Last Name' },
      { accessorKey: 'age', header: 'Age' }
    ]
     
    export function PeopleTable({ data }: { data: Person[] }) {
      const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
      return (
        <table>
          <thead>
            {table.getHeaderGroups().map(hg => (
              <tr key={hg.id}>
                {hg.headers.map(h => <th key={h.id}>{h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}</th>)}
              </tr>
            ))}
          </thead>
          <tbody>
            {table.getRowModel().rows.map(row => (
              <tr key={row.id}>
                {row.getVisibleCells().map(c => <td key={c.id}>{flexRender(c.column.columnDef.cell, c.getContext())}</td>)}
              </tr>
            ))}
          </tbody>
        </table>
      )
    }
  • Row Processing Features:
    • Client-side: Controlled state sorting/filtering/pagination via getSortedRowModel, getFilteredRowModel, getPaginationRowModel.
      const [sorting, setSorting] = React.useState<SortingState>([])
      const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 20 })
       
      const table = useReactTable({
        data, columns, state: { sorting, pagination },
        onSortingChange: setSorting, onPaginationChange: setPagination,
        getCoreRowModel: getCoreRowModel(),
        getSortedRowModel: getSortedRowModel(),
        getPaginationRowModel: getPaginationRowModel(),
      })
    • Server-side sorting/pagination:
      • Configuration: Set manualSorting: true, manualPagination: true.
      • Setup: Feed sorting/pagination state into API queryKey (caching handled by TanStack Query).
    • Pagination gotchas:
      • autoResetPageIndex: Defaults to true (pageIndex resets on data/filter updates). manualPagination automatically disables this.
      • State collision: Never pass pagination to both state and initialState (state takes priority).
  • Virtualization (Big Tables): Headless pair with TanStack Virtual; virtualize rows while keeping headers sticky.

πŸ§ βš™οΈ Architecture

  • HTTP: Single client/API module.
  • Queries: Dedicated Query Key factory module + queryOptions.
  • Structure: Features folders containing routes, queries, and components.
  • Validation: Zod for route params and API payloads.

πŸ”— Resources