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.

Zur Installation springen

Quellinformationen

Repository
hashintel/brunch
Letzte Quellaktivität
16. April 2026 um 10:34
Erkannte Sprache von SKILL.md
Englisch
Sterne
7
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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:
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen