Loading...
Loading...
Automatic code splitting (autoCodeSplitting), .lazy.tsx convention, createLazyFileRoute, createLazyRoute, lazyRouteComponent, getRouteApi for typed hooks in split files, codeSplitGroupings per-route override, splitBehavior programmatic config, critical vs non-critical properties.
npx skill4agent add tanstack/router code-splitting.lazy.tsxCRITICAL: Nevercomponent functions from route files — exported functions are included in the main bundle and bypass code splitting entirely.export
CRITICAL: Usein code-split files, NOTgetRouteApi('/path'). Importing Route defeats code splitting.import { Route } from './route'
validateSearchloaderbeforeLoadcomponenterrorComponentpendingComponentnotFoundComponentTheis NOT split by default. It is already async, so splitting it adds a double async cost: fetch the chunk, then execute the loader. Only split the loader if you have a specific reason.loader
autoCodeSplitting: true// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
// TanStack Router plugin MUST come before the framework plugin
tanstackRouter({
autoCodeSplitting: true,
}),
react(),
],
}).lazy.tsx// src/routes/posts.tsx — everything in one file, splitting is automatic
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: PostsComponent,
})
// NOT exported — this is critical for automatic code splitting to work
function PostsComponent() {
const posts = Route.useLoaderData()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}.lazy.tsx// src/routes/posts.tsx — critical route config only
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
})// src/routes/posts.lazy.tsx — non-critical (lazy-loaded)
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
// Use getRouteApi to access typed hooks without importing Route
return <div>Posts</div>
}createLazyFileRoutecomponenterrorComponentpendingComponentnotFoundComponentrouteTree.gen.ts// src/routes/about.lazy.tsx — no about.tsx needed
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/about')({
component: () => <h1>About Us</h1>,
})createLazyRoute.lazy()// src/posts.lazy.tsx
import { createLazyRoute } from '@tanstack/react-router'
export const Route = createLazyRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
return <div>Posts</div>
}// src/app.tsx
import { createRoute } from '@tanstack/react-router'
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
}).lazy(() => import('./posts.lazy').then((d) => d.Route))getRouteApigetRouteApi// src/routes/posts.lazy.tsx
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
const posts = routeApi.useLoaderData()
const { page } = routeApi.useSearch()
const params = routeApi.useParams()
const context = routeApi.useRouteContext()
return <div>Posts page {page}</div>
}getRouteApiuseLoaderDatauseLoaderDepsuseMatchuseParamsuseRouteContextuseSearchcodeSplitGroupingscodeSplitGroupings// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { loadPostsData } from './-heavy-posts-utils'
export const Route = createFileRoute('/posts')({
// Bundle loader and component together for this route
codeSplitGroupings: [['loader', 'component']],
loader: () => loadPostsData(),
component: PostsComponent,
})
function PostsComponent() {
const data = Route.useLoaderData()
return <div>{data.title}</div>
}defaultBehavior// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
codeSplittingOptions: {
defaultBehavior: [
// Bundle all UI components into one chunk
[
'component',
'pendingComponent',
'errorComponent',
'notFoundComponent',
],
],
},
}),
],
})splitBehavior// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
codeSplittingOptions: {
splitBehavior: ({ routeId }) => {
if (routeId.startsWith('/posts')) {
return [['loader', 'component']]
}
// All other routes use defaultBehavior
},
},
}),
],
})codeSplitGroupingssplitBehaviordefaultBehavior// WRONG — export puts PostsComponent in the main bundle
export function PostsComponent() {
return <div>Posts</div>
}
// CORRECT — no export, function stays in the split chunk
function PostsComponent() {
return <div>Posts</div>
}__root.tsx__root.lazy.tsx// AVOID unless you have a specific reason
codeSplittingOptions: {
defaultBehavior: [
['loader'], // Fetch chunk THEN execute loader = two network waterfalls
['component'],
],
}
// PREFERRED — loader stays in main bundle (default behavior)
codeSplittingOptions: {
defaultBehavior: [
['component'],
['errorComponent'],
['notFoundComponent'],
],
}// WRONG — importing Route pulls route config into the lazy chunk
import { Route } from './posts.tsx'
const data = Route.useLoaderData()
// CORRECT — getRouteApi gives typed hooks without pulling in the route
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()getRouteApi