Files
findyourpilot/.agents/skills/tanstack-router-best-practices/rules/split-lazy-routes.md
2026-03-02 21:16:26 +01:00

3.9 KiB

split-lazy-routes: Use .lazy.tsx for Code Splitting

Priority: MEDIUM

Explanation

Split route components into .lazy.tsx files to reduce initial bundle size. The main route file keeps critical configuration (path, loaders, search validation), while lazy files contain components that load on-demand.

Bad Example

// routes/dashboard.tsx - Everything in one file
import { createFileRoute } from '@tanstack/react-router'
import { HeavyChartLibrary } from 'heavy-chart-library'
import { ComplexDataGrid } from 'complex-data-grid'
import { AnalyticsWidgets } from './components/AnalyticsWidgets'

export const Route = createFileRoute('/dashboard')({
  loader: async ({ context }) => {
    return context.queryClient.ensureQueryData(dashboardQueries.stats())
  },
  component: DashboardPage,  // Entire component in main bundle
})

function DashboardPage() {
  // Heavy components loaded even if user never visits dashboard
  return (
    <div>
      <HeavyChartLibrary data={useLoaderData()} />
      <ComplexDataGrid />
      <AnalyticsWidgets />
    </div>
  )
}

Good Example

// routes/dashboard.tsx - Only critical config
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
  loader: async ({ context }) => {
    return context.queryClient.ensureQueryData(dashboardQueries.stats())
  },
  // No component - it's in the lazy file
})

// routes/dashboard.lazy.tsx - Lazy-loaded component
import { createLazyFileRoute } from '@tanstack/react-router'
import { HeavyChartLibrary } from 'heavy-chart-library'
import { ComplexDataGrid } from 'complex-data-grid'
import { AnalyticsWidgets } from './components/AnalyticsWidgets'

export const Route = createLazyFileRoute('/dashboard')({
  component: DashboardPage,
  pendingComponent: DashboardSkeleton,
  errorComponent: DashboardError,
})

function DashboardPage() {
  const data = Route.useLoaderData()
  return (
    <div>
      <HeavyChartLibrary data={data} />
      <ComplexDataGrid />
      <AnalyticsWidgets />
    </div>
  )
}

function DashboardSkeleton() {
  return <div className="dashboard-skeleton">Loading dashboard...</div>
}

function DashboardError({ error }: { error: Error }) {
  return <div>Failed to load dashboard: {error.message}</div>
}

What Goes Where

// Main route file (routes/example.tsx)
// - path configuration (implicit from file location)
// - validateSearch
// - beforeLoad (auth checks, redirects)
// - loader (data fetching)
// - loaderDeps
// - context manipulation
// - Static route data

// Lazy file (routes/example.lazy.tsx)
// - component
// - pendingComponent
// - errorComponent
// - notFoundComponent

Using getRouteApi in Lazy Components

// routes/posts/$postId.lazy.tsx
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'

const route = getRouteApi('/posts/$postId')

export const Route = createLazyFileRoute('/posts/$postId')({
  component: PostPage,
})

function PostPage() {
  // Type-safe access without importing main route file
  const { postId } = route.useParams()
  const data = route.useLoaderData()

  return <article>{/* ... */}</article>
}

Automatic Code Splitting

// vite.config.ts - Enable automatic splitting
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    TanStackRouterVite({
      autoCodeSplitting: true,  // Automatically splits all route components
    }),
    react(),
  ],
})

// With autoCodeSplitting, you don't need .lazy.tsx files
// The plugin handles the splitting automatically

Context

  • Lazy loading reduces initial bundle size significantly
  • Loaders are NOT lazy - they need to run before rendering
  • createLazyFileRoute only accepts component-related options
  • Use getRouteApi() for type-safe hook access in lazy files
  • Consider autoCodeSplitting: true for simpler setup
  • Virtual routes auto-generate when only .lazy.tsx exists