Loading...
Loading...
Route loader option, loaderDeps for cache keys, staleTime/gcTime/ defaultPreloadStaleTime SWR caching, pendingComponent/pendingMs/ pendingMinMs, errorComponent/onError/onCatch, beforeLoad, router context and createRootRouteWithContext DI pattern, router.invalidate, Await component, deferred data loading with unawaited promises.
npx skill4agent add tanstack/router data-loadinguseLoaderData// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
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>
)
}getRouteApiimport { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
function PostsComponent() {
const posts = routeApi.useLoaderData()
return <ul>{/* ... */}</ul>
}route.params.parseroute.validateSearchroute.beforeLoadroute.onErrorroute.errorComponentroute.component.preload?route.loaderroute.pendingComponentroute.componentroute.onErrorroute.errorComponentbeforeLoadloaderbeforeLoadbeforeLoadbeforeLoadloaderDeps// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
validateSearch: (search) => ({
offset: Number(search.offset) || 0,
limit: Number(search.limit) || 10,
}),
loaderDeps: ({ search: { offset, limit } }) => ({ offset, limit }),
loader: ({ deps: { offset, limit } }) => fetchPosts({ offset, limit }),
})staleTimeloaderDepsstaleTimepreloadStaleTimegcTimeexport const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 10_000, // 10s: data considered fresh for 10 seconds
gcTime: 5 * 60 * 1000, // 5min: garbage collect after 5 minutes
})export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: Infinity,
})const router = createRouter({
routeTree,
defaultStaleTime: Infinity,
})pendingMs: 1000pendingMinMs: 500export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
pendingMs: 500,
pendingMinMs: 300,
pendingComponent: () => <div>Loading posts...</div>,
component: PostsComponent,
})createRootRouteWithContext// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface MyRouterContext {
auth: { userId: string }
fetchPosts: () => Promise<Post[]>
}
// NOTE: double call — createRootRouteWithContext<Type>()({...})
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => <Outlet />,
})// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
context: {
auth: { userId: '123' },
fetchPosts,
},
})// src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})RouterProvidercontextimport { RouterProvider } from '@tanstack/react-router'
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}beforeLoadexport const Route = createFileRoute('/posts')({
beforeLoad: ({ context }) => ({
fetchPosts: context.fetchPosts,
}),
loader: ({ context: { fetchPosts } }) => fetchPosts(),
})fetch('/api/posts')import { createServerFn } from '@tanstack/react-start'
const getPosts = createServerFn({ method: 'GET' }).handler(() => {
return db.posts.findMany()
})
export const Route = createFileRoute('/posts')({
loader: () => getPosts(),
})Awaitimport { createFileRoute, Await } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params: { postId } }) => {
// Slow data — do NOT await
const slowDataPromise = fetchComments(postId)
// Fast data — await
const post = await fetchPost(postId)
return { post, deferredComments: slowDataPromise }
},
component: PostComponent,
})
function PostComponent() {
const { post, deferredComments } = Route.useLoaderData()
return (
<div>
<h1>{post.title}</h1>
<Await
promise={deferredComments}
fallback={<div>Loading comments...</div>}
>
{(comments) => (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.body}</li>
))}
</ul>
)}
</Await>
</div>
)
}router.invalidate()import { useRouter } from '@tanstack/react-router'
function AddPostButton() {
const router = useRouter()
const handleAdd = async () => {
await createPost({ title: 'New post' })
await router.invalidate({ sync: true })
}
return <button onClick={handleAdd}>Add Post</button>
}await router.invalidate({ sync: true })import {
createFileRoute,
ErrorComponent,
useRouter,
} from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
errorComponent: ({ error, reset }) => {
const router = useRouter()
if (error instanceof CustomError) {
return <div>{error.message}</div>
}
return (
<div>
<ErrorComponent error={error} />
<button
onClick={() => {
// For loader errors, invalidate to re-run loader + reset boundary
router.invalidate()
}}
>
Retry
</button>
</div>
)
},
})loaderparamsdepsloaderDepscontextabortControllercause'enter''stay''preload'preloadtruelocationparentMatchPromiserouteexport const Route = createFileRoute('/posts/$postId')({
loader: ({ params: { postId }, abortController }) =>
fetchPost(postId, { signal: abortController.signal }),
})// WRONG — this will crash in the browser
export const Route = createFileRoute('/posts')({
loader: async () => {
const fs = await import('fs') // Node.js only!
return JSON.parse(fs.readFileSync('...')) // fails in browser
},
})
// CORRECT for an SPA — use a client-safe API helper
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})fetch('/api/...')staleTime0staleTimeexport const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
staleTime: 60_000, // Consider fresh for 1 minute
})reset()router.invalidate()// WRONG — resets boundary but loader still has stale error
function PostErrorComponent({ error, reset }) {
return <button onClick={reset}>Retry</button>
}
// CORRECT — re-runs loader and resets the error boundary
function PostErrorComponent({ error }) {
const router = useRouter()
return <button onClick={() => router.invalidate()}>Retry</button>
}createRootRouteWithContext<Type>()// WRONG — missing second call, passes options to the factory
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>({
component: RootComponent,
})
// CORRECT — factory()({options})
const rootRoute = createRootRouteWithContext<{ auth: AuthState }>()({
component: RootComponent,
})beforeLoadloader// WRONG — hooks cannot be called outside React components
export const Route = createFileRoute('/posts')({
loader: () => {
const auth = useAuth() // This will crash!
return fetchPosts(auth.userId)
},
})
// CORRECT — inject hook values via router context
// In your App component:
function InnerApp() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In your route:
export const Route = createFileRoute('/posts')({
loader: ({ context: { auth } }) => fetchPosts(auth.userId),
})beforeLoadloaderbeforeLoad// WRONG — context.user is unknown because beforeLoad declared after loader
export const Route = createFileRoute('/admin')({
loader: ({ context }) => fetchData(context.user),
beforeLoad: () => ({ user: getUser() }),
})
// CORRECT — validateSearch → loaderDeps → beforeLoad → loader
export const Route = createFileRoute('/admin')({
beforeLoad: () => ({ user: getUser() }),
loader: ({ context }) => fetchData(context.user),
})// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search
// CORRECT — only re-run when page changes
loaderDeps: ({ search }) => ({ page: search.page })searchsortDirectionviewModedefaultPreloadStaleTime: 0loaderDeps