Skip to main content

router-core-data-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.

Source facts

Repository
hasharchives/brunch
Last source activity
April 16, 2026 at 10:34
Detected SKILL.md language
English
Stars
9
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
router-core-data-loading
description
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.
type
sub-skill
library
tanstack-router
library_version
1.166.2
requires
["router-core"]
sources
["TanStack/router:docs/router/guide/data-loading.md","TanStack/router:docs/router/guide/deferred-data-loading.md","TanStack/router:docs/router/guide/router-context.md","TanStack/router:docs/router/guide/data-mutations.md"]
# Data Loading ## Setup Basic loader returning data, consumed via `useLoaderData`: ```tsx // 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> ) } ``` In code-split components, use `getRouteApi` instead of importing Route: ```tsx import { getRouteApi } from '@tanstack/react-router' const routeApi = getRouteApi('/posts') function PostsComponent() { const posts = routeApi.useLoaderData() return <ul>{/* ... */}</ul> } ``` ## Route Loading Lifecycle The router executes this sequence on every URL/history update: 1. **Route Matching** (top-down) - `route.params.parse` - `route.validateSearch` 2. **Route Pre-Loading** (serial) - `route.beforeLoad` - `route.onError` → `route.errorComponent` 3. **Route Loading** (parallel) - `route.component.preload?` - `route.loader` - `route.pendingComponent` (optional) - `route.component` - `route.onError` → `route.errorComponent` Key: `beforeLoad` runs before `loader`. `beforeLoad` for a parent runs before its children's `beforeLoad`. Throwing in `beforeLoad` prevents all children from loading. ## Core Patterns ### loaderDeps for Search-Param-Driven Cache Keys Loaders don't receive search params directly. Use `loaderDeps` to declare which search params affect the cache key: ```tsx // 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 }), }) ``` When deps change, the route reloads regardless of `staleTime`. ### SWR Caching Configuration TanStack Router has built-in Stale-While-Revalidate caching keyed on the route's parsed pathname + `loaderDeps`. Defaults: - **`staleTime`: 0** — data is always considered stale, reloads in background on re-match - **`preloadStaleTime`: 30 seconds** — preloaded data won't be refetched for 30s - **`gcTime`: 30 minutes** — unused cache entries garbage collected after 30min ```tsx export 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 }) ``` Disable SWR caching entirely: ```tsx export const Route = createFileRoute('/posts')({ loader: () => fetchPosts(), staleTime: Infinity, }) ``` Globally: ```tsx const router = createRouter({ routeTree, defaultStaleTime: Infinity, }) ``` ### Pending States (pendingComponent / pendingMs / pendingMinMs) By default, a pending component shows after 1 second (`pendingMs: 1000`) and stays for at least 500ms (`pendingMinMs: 500`) to avoid flash. ```tsx export const Route = createFileRoute('/posts')({ loader: () => fetchPosts(), pendingMs: 500, pendingMinMs: 300, pendingComponent: () => <div>Loading posts...</div>, component: PostsComponent, }) ``` ### Router Context with createRootRouteWithContext (Factory Pattern) `createRootRouteWithContext` is a factory that returns a function. You must call it twice — the first call passes the generic type, the second passes route options: ```tsx // 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 />, }) ``` Supply the context when creating the router: ```tsx // src/router.tsx import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' const router = createRouter({ routeTree, context: { auth: { userId: '123' }, fetchPosts, }, }) ``` Consume in loaders and beforeLoad: ```tsx // src/routes/posts.tsx export const Route = createFileRoute('/posts')({ loader: ({ context: { fetchPosts } }) => fetchPosts(), }) ``` To pass React hook values into the router context, call the hook above `RouterProvider` and inject via the `context` prop: ```tsx import { RouterProvider } from '@tanstack/react-router' function InnerApp() { const auth = useAuth() return <RouterProvider router={router} context={{ auth }} /> } function App() { return ( <AuthProvider> <InnerApp /> </AuthProvider> ) } ``` Route-level context via `beforeLoad`: ```tsx export const Route = createFileRoute('/posts')({ beforeLoad: () => ({ fetchPosts: () => fetch('/api/posts').then((r) => r.json()), }), loader: ({ context: { fetchPosts } }) => fetchPosts(), }) ``` ### Deferred Data Loading Return unawaited promises from the loader for non-critical data. Use the `Await` component to render them: ```tsx import { 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> ) } ``` ### Invalidation After Mutations `router.invalidate()` forces all active route loaders to re-run and marks all cached data as stale: ```tsx import { useRouter } from '@tanstack/react-router' function AddPostButton() { const router = useRouter() const handleAdd = async () => { await fetch('/api/posts', { method: 'POST', body: '...' }) router.invalidate() } return <button onClick={handleAdd}>Add Post</button> } ``` For synchronous invalidation (wait until loaders finish): ```tsx await router.invalidate({ sync: true }) ``` ### Error Handling ```tsx 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> ) }, }) ``` ### Loader Parameters The `loader` function receives: - `params` — parsed path params - `deps` — object from `loaderDeps` - `context` — merged parent + beforeLoad context - `abortController` — cancelled when route unloads or becomes stale - `cause` — `'enter'`, `'stay'`, or `'preload'` - `preload` — `true` during preloading - `location` — current location object - `parentMatchPromise` — promise of parent route match - `route` — the route object itself ```tsx export const Route = createFileRoute('/posts/$postId')({ loader: ({ params: { postId }, abortController }) => fetchPost(postId, { signal: abortController.signal }), }) ``` ## Common Mistakes ### CRITICAL: Assuming loaders only run on the server TanStack Router is **client-first**. Loaders run on the **client** by default. They also run on the server when using TanStack Start for SSR, but the default mental model is client-side execution. ```tsx // 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 — loaders run in the browser, use fetch or API calls export const Route = createFileRoute('/posts')({ loader: async () => { const res = await fetch('/api/posts') return res.json() }, }) ``` Do NOT put database queries, filesystem access, or server-only code in loaders unless you are using TanStack Start server functions. ### MEDIUM: Not understanding staleTime default is 0 Default `staleTime` is `0`. This means data reloads in the background on every route re-match. This is intentional — it ensures fresh data. But if your data is expensive or static, set `staleTime`: ```tsx export const Route = createFileRoute('/posts')({ loader: () => fetchPosts(), staleTime: 60_000, // Consider fresh for 1 minute }) ``` ### HIGH: Using reset() instead of router.invalidate() in error components `reset()` only resets the error boundary UI. It does NOT re-run the loader. For loader errors, use `router.invalidate()` which re-runs loaders and resets the boundary: ```tsx // 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> } ``` ### HIGH: Missing double parentheses on createRootRouteWithContext `createRootRouteWithContext<Type>()` is a factory — it returns a function. Must call twice: ```tsx // 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, }) ``` ### HIGH: Using React hooks in beforeLoad or loader `beforeLoad` and `loader` are NOT React components. You cannot call hooks inside them. Use router context to inject values from hooks: ```tsx // 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), }) ``` ### HIGH: Property order affects TypeScript inference Router infers types from earlier properties into later ones. Declaring `beforeLoad` after `loader` means context from `beforeLoad` is unknown in the loader:
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub