| name | router-core/code-splitting |
| description | 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. |
| type | sub-skill |
| library | tanstack-router |
| library_version | 1.166.2 |
| requires | ["router-core"] |
| sources | ["TanStack/router:docs/router/guide/code-splitting.md","TanStack/router:docs/router/guide/automatic-code-splitting.md"] |
Code Splitting
TanStack Router separates route code into critical (required to match and start loading) and non-critical (can be lazy-loaded). The bundler plugin can split automatically, or you can split manually with .lazy.tsx files.
CRITICAL: Never export component functions from route files — exported functions are included in the main bundle and bypass code splitting entirely.
CRITICAL: Use getRouteApi('/path') in code-split files, NOT import { Route } from './route'. Importing Route defeats code splitting.
What Stays in the Main Bundle (Critical)
- Path parsing/serialization
validateSearch
loader, beforeLoad
- Route context, static data
- Links, scripts, styles
What Gets Split (Non-Critical)
component
errorComponent
pendingComponent
notFoundComponent
The loader is 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.
Setup: Automatic Code Splitting
Enable autoCodeSplitting: true in the bundler plugin. This is the recommended approach.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
}),
react(),
],
})
With this enabled, route files are automatically transformed. Components are split into separate chunks; loaders stay in the main bundle. No .lazy.tsx files needed.
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: PostsComponent,
})
function PostsComponent() {
const posts = Route.useLoaderData()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Manual Splitting with .lazy.tsx
If you cannot use automatic code splitting (e.g. CLI-only, no bundler plugin), split manually into two files:
import { createFileRoute } from '@tanstack/react-router'
import { fetchPosts } from '../api'
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
})
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
return <div>Posts</div>
}
createLazyFileRoute supports only: component, errorComponent, pendingComponent, notFoundComponent.
Virtual Routes
If splitting leaves the critical route file empty, delete it entirely. A virtual route is auto-generated in routeTree.gen.ts:
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/about')({
component: () => <h1>About Us</h1>,
})
Code-Based Splitting
For code-based (non-file-based) routing, use createLazyRoute and the .lazy() method:
import { createLazyRoute } from '@tanstack/react-router'
export const Route = createLazyRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
return <div>Posts</div>
}
import { createRoute } from '@tanstack/react-router'
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
Accessing Typed Hooks in Split Files: getRouteApi
When your component lives in a separate file, use getRouteApi to get typed access to route hooks without importing the Route object:
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>
}
getRouteApi provides: useLoaderData, useLoaderDeps, useMatch, useParams, useRouteContext, useSearch.
Per-Route Split Overrides: codeSplitGroupings
Override split behavior for a specific route by adding codeSplitGroupings directly in the route file:
import { createFileRoute } from '@tanstack/react-router'
import { loadPostsData } from './-heavy-posts-utils'
export const Route = createFileRoute('/posts')({
codeSplitGroupings: [['loader', 'component']],
loader: () => loadPostsData(),
component: PostsComponent,
})
function PostsComponent() {
const data = Route.useLoaderData()
return <div>{data.title}</div>
}
Global Split Configuration
defaultBehavior — Change Default Groupings
import { defineConfig } from 'vite'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({
autoCodeSplitting: true,
codeSplittingOptions: {
defaultBehavior: [
[
'component',
'pendingComponent',
'errorComponent',
'notFoundComponent',
],
],
},
}),
],
})
splitBehavior — Programmatic Per-Route Logic
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']]
}
},
},
}),
],
})
Precedence Order
- Per-route
codeSplitGroupings (highest)
splitBehavior function
defaultBehavior option (lowest)
Common Mistakes
1. HIGH: Exporting component functions prevents code splitting
export function PostsComponent() {
return <div>Posts</div>
}
function PostsComponent() {
return <div>Posts</div>
}
2. MEDIUM: Trying to code-split the root route
__root.tsx does not support code splitting. It is always rendered regardless of the current route. Do not create __root.lazy.tsx.
3. MEDIUM: Splitting the loader adds double async cost
codeSplittingOptions: {
defaultBehavior: [
['loader'],
['component'],
],
}
codeSplittingOptions: {
defaultBehavior: [
['component'],
['errorComponent'],
['notFoundComponent'],
],
}
4. HIGH: Importing Route in code-split files for typed hooks
import { Route } from './posts.tsx'
const data = Route.useLoaderData()
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
const data = routeApi.useLoaderData()
Cross-References
- router-core/data-loading — Loader splitting decisions affect data loading performance. Splitting the loader adds latency before data can be fetched.
- router-core/type-safety —
getRouteApi is the type-safe way to access hooks from split files.