RO

router-query-integration

Optimizes SPA performance by integrating TanStack Router loaders with TanStack Query prefetching.

Install

mkdir -p .claude/skills/router-query-integration && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/5802" && unzip -o skill.zip -d .claude/skills/router-query-integration && rm skill.zip

Installs to .claude/skills/router-query-integration

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Use when setting up route loaders or optimizing navigation performance. Integrates TanStack Router with TanStack Query for optimal data fetching. Covers route loaders with query prefetching, ensuring instant navigation, and eliminating request waterfalls.
255 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Implements TanStack query loaders
  • Prefetches data during navigation
  • Reduces request waterfalls
  • Configures staleTime for cache management

How it works

It injects query prefetching into the router's loader lifecycle to ensure data availability prior to component rendering.

Inputs & outputs

You give it
Component path and data fetching requirements
You get back
Prefetching logic integrated into route loader

When to use router-query-integration

  • Setting up route loaders
  • Optimizing navigation performance
  • Prefetching queries in SPA routes

About this skill

Router × Query Integration

Seamlessly integrate TanStack Router with TanStack Query for optimal SPA performance and instant navigation.

Route Loader + Query Prefetch

The key pattern: Use route loaders to prefetch queries BEFORE navigation completes.

Benefits:

  • Loaders run before render, eliminating waterfall
  • Fast SPA navigations (instant perceived performance)
  • Queries still benefit from cache deduplication
  • Add Router & Query DevTools during development (auto-hide in production)

Basic Pattern

// src/routes/users/$id.tsx
import { createFileRoute } from '@tanstack/react-router'
import { queryClient } from '@/app/queryClient'
import { usersKeys, fetchUser } from '@/features/users/queries'

export const Route = createFileRoute('/users/$id')({
  loader: async ({ params }) => {
    const id = params.id

    return queryClient.ensureQueryData({
      queryKey: usersKeys.detail(id),
      queryFn: () => fetchUser(id),
      staleTime: 30_000, // Fresh for 30 seconds
    })
  },
  component: UserPage,
})

function UserPage() {
  const { id } = Route.useParams()
  const { data: user } = useQuery({
    queryKey: usersKeys.detail(id),
    queryFn: () => fetchUser(id),
  })

  // Data is already loaded from loader, so this returns instantly
  return <div>{user.name}</div>
}

Using Query Options Pattern (Recommended)

Query Options provide maximum type safety and DRY:

// features/users/queries.ts
import { queryOptions } from '@tanstack/react-query'

export function userQueryOptions(userId: string) {
  return queryOptions({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 30_000,
  })
}

export function useUser(userId: string) {
  return useQuery(userQueryOptions(userId))
}

// src/routes/users/$userId.tsx
import { userQueryOptions } from '@/features/users/queries'
import { queryClient } from '@/app/queryClient'

export const Route = createFileRoute('/users/$userId')({
  loader: ({ params }) =>
    queryClient.ensureQueryData(userQueryOptions(params.userId)),
  component: UserPage,
})

function UserPage() {
  const { userId } = Route.useParams()
  const { data: user } = useUser(userId)

  return <div>{user.name}</div>
}

Multiple Queries in Loader

export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    // Run in parallel
    await Promise.all([
      queryClient.ensureQueryData(userQueryOptions()),
      queryClient.ensureQueryData(statsQueryOptions()),
      queryClient.ensureQueryData(postsQueryOptions()),
    ])
  },
  component: Dashboard,
})

function Dashboard() {
  const { data: user } = useUser()
  const { data: stats } = useStats()
  const { data: posts } = usePosts()

  // All data pre-loaded, renders instantly
  return (
    <div>
      <UserHeader user={user} />
      <StatsPanel stats={stats} />
      <PostsList posts={posts} />
    </div>
  )
}

Dependent Queries

export const Route = createFileRoute('/users/$userId/posts')({
  loader: async ({ params }) => {
    // First ensure user data
    const user = await queryClient.ensureQueryData(
      userQueryOptions(params.userId)
    )

    // Then fetch user's posts
    return queryClient.ensureQueryData(
      userPostsQueryOptions(user.id)
    )
  },
  component: UserPostsPage,
})

Query Client Setup

Export the query client for use in loaders:

// src/app/queryClient.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 0,
      gcTime: 5 * 60_000,
      retry: 1,
    },
  },
})

// src/main.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from './app/queryClient'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  </StrictMode>
)

Prefetch vs Ensure

prefetchQuery - Fire and forget, don't wait:

loader: ({ params }) => {
  // Don't await - just start fetching
  queryClient.prefetchQuery(userQueryOptions(params.userId))
  // Navigation continues immediately
}

ensureQueryData - Wait for data (recommended):

loader: async ({ params }) => {
  // Await - navigation waits until data is ready
  return await queryClient.ensureQueryData(userQueryOptions(params.userId))
}

fetchQuery - Always fetches fresh:

loader: async ({ params }) => {
  // Ignores cache, always fetches
  return await queryClient.fetchQuery(userQueryOptions(params.userId))
}

Recommendation: Use ensureQueryData for most cases - respects cache and staleTime.

Handling Errors in Loaders

export const Route = createFileRoute('/users/$userId')({
  loader: async ({ params }) => {
    try {
      return await queryClient.ensureQueryData(userQueryOptions(params.userId))
    } catch (error) {
      // Let router error boundary handle it
      throw error
    }
  },
  errorComponent: ({ error }) => (
    <div>
      <h1>Failed to load user</h1>
      <p>{error.message}</p>
    </div>
  ),
  component: UserPage,
})

Invalidating Queries After Mutations

// features/users/mutations.ts
export function useUpdateUser() {
  const queryClient = useQueryClient()
  const navigate = useNavigate()

  return useMutation({
    mutationFn: (user: UpdateUserDTO) => api.put(`/users/${user.id}`, user),
    onSuccess: (updatedUser) => {
      // Update cache immediately
      queryClient.setQueryData(
        userQueryOptions(updatedUser.id).queryKey,
        updatedUser
      )

      // Invalidate related queries
      queryClient.invalidateQueries({ queryKey: ['users', 'list'] })

      // Navigate to updated user page (will use cached data)
      navigate({ to: '/users/$userId', params: { userId: updatedUser.id } })
    },
  })
}

Preloading on Link Hover

import { Link, useRouter } from '@tanstack/react-router'

function UserLink({ userId }: { userId: string }) {
  const router = useRouter()

  const handleMouseEnter = () => {
    // Preload route (includes loader)
    router.preloadRoute({ to: '/users/$userId', params: { userId } })
  }

  return (
    <Link
      to="/users/$userId"
      params={{ userId }}
      onMouseEnter={handleMouseEnter}
    >
      View User
    </Link>
  )
}

Or use built-in preload:

<Link
  to="/users/$userId"
  params={{ userId: '123' }}
  preload="intent" // Preload on hover/focus
>
  View User
</Link>

Search Params + Queries

// src/routes/users/index.tsx
import { z } from 'zod'

const searchSchema = z.object({
  page: z.number().default(1),
  filter: z.enum(['active', 'all']).default('all'),
})

export const Route = createFileRoute('/users/')({
  validateSearch: searchSchema,
  loader: ({ search }) => {
    return queryClient.ensureQueryData(
      usersListQueryOptions(search.page, search.filter)
    )
  },
  component: UsersPage,
})

function UsersPage() {
  const { page, filter } = Route.useSearch()
  const { data: users } = useUsersList(page, filter)

  return <UserTable users={users} page={page} filter={filter} />
}

Suspense Mode

With Suspense, you don't need separate loading states:

export const Route = createFileRoute('/users/$userId')({
  loader: ({ params }) =>
    queryClient.ensureQueryData(userQueryOptions(params.userId)),
  component: UserPage,
})

function UserPage() {
  const { userId } = Route.useParams()

  // Use Suspense hook - data is NEVER undefined
  const { data: user } = useSuspenseQuery(userQueryOptions(userId))

  return <div>{user.name}</div>
}

// Wrap route in Suspense boundary (in __root.tsx or layout)
<Suspense fallback={<Spinner />}>
  <Outlet />
</Suspense>

Performance Best Practices

  1. Prefetch in Loaders - Always use loaders to eliminate waterfalls
  2. Use Query Options - Share configuration between loaders and components
  3. Set Appropriate staleTime - Tune per query (30s for user data, 10min for static)
  4. Parallel Prefetching - Use Promise.all() for independent queries
  5. Hover Preloading - Enable preload="intent" on critical links
  6. Cache Invalidation - Be specific with invalidation keys to avoid unnecessary refetches

DevTools Setup

// src/main.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'

<QueryClientProvider client={queryClient}>
  <RouterProvider router={router} />
  <ReactQueryDevtools position="bottom-right" />
  <TanStackRouterDevtools position="bottom-left" />
</QueryClientProvider>

Both auto-hide in production.

Common Patterns

List + Detail Pattern:

// List route prefetches list
export const ListRoute = createFileRoute('/users/')({
  loader: () => queryClient.ensureQueryData(usersListQueryOptions()),
  component: UsersList,
})

// Detail route prefetches specific user
export const DetailRoute = createFileRoute('/users/$userId')({
  loader: ({ params }) =>
    queryClient.ensureQueryData(userQueryOptions(params.userId)),
  component: UserDetail,
})

// Clicking from list to detail uses cached data if available

Edit Form Pattern:

export const EditRoute = createFileRoute('/users/$userId/edit')({
  loader: ({ params }) =>
    queryClient.ensureQueryData(userQueryOptions(params.userId)),
  component: UserEditForm,
})

function UserEditForm() {
  const { userId } = Route.useParams()
  const { data: user } = useUser(userId)
  const updateUser = useUpdateUser()

  // Form pre-populated with cached user data
  return <Form initialValues={user} onSubmit={updateUser.mutate} />
}

Related Skills

  • tanstack-query - Comprehensive Query v5 patterns
  • tanstack-router - Router

Content truncated.

When not to use it

  • In static, data-agnostic sites
  • When not using TanStack tools

Prerequisites

TanStack RouterTanStack Query

Limitations

  • Depends on specific TanStack ecosystem versions
  • Adds complexity to route files

How it compares

It optimizes data delivery timing relative to the navigation process rather than just fetching on mount.

Compared to similar skills

router-query-integration side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
router-query-integration (this skill)16moNo flagsAdvanced
nextjs-developer3282moNo flagsAdvanced
frontend-developer274moNo flagsIntermediate
rendering-animate-svg136moNo flagsBeginner

Try saying

Example prompts that trigger this skill in your AI assistant.

More by MadAppGang

View all by MadAppGang

claudish-usage

MadAppGang

CRITICAL - Guide for using Claudish CLI ONLY through sub-agents to run Claude Code with any AI model (OpenRouter, Gemini, OpenAI, local models). NEVER run Claudish directly in main context unless user explicitly requests it. Use when user mentions external AI models, Claudish, OpenRouter, Gemini, OpenAI, Ollama, or alternative models. Includes mandatory sub-agent delegation patterns, agent selection guide, file-based instructions, and strict rules to prevent context window pollution.

442

golang-performance

MadAppGang

Use when profiling Go applications (pprof), running benchmarks, optimizing memory/CPU usage, or debugging performance bottlenecks in production Go code.

47

golang

MadAppGang

Use when building Go backend services, implementing goroutines/channels, handling errors idiomatically, writing tests with testify, or following Go best practices for APIs/CLI tools.

313

schemas

MadAppGang

YAML frontmatter schemas for Claude Code agents and commands. Use when creating or validating agent/command files.

34

external-model-selection

MadAppGang

Choose optimal external AI models for code analysis, bug investigation, and architectural decisions. Use when consulting multiple LLMs via claudish, comparing model perspectives, or investigating complex Go/LSP/transpiler issues. Provides empirically validated model rankings (91/100 for MiniMax M2, 83/100 for Grok Code Fast) and proven consultation strategies based on real-world testing.

218

adr-documentation

MadAppGang

Architecture Decision Records (ADR) documentation practice. Use when documenting architectural decisions, recording technical trade-offs, creating decision logs, or establishing architectural patterns. Trigger keywords - "ADR", "architecture decision", "decision record", "trade-offs", "architectural decision", "decision log".

12

Search skills

Search the agent skills registry