본문으로 건너뛰기
cccskills
무료GitHub에 공개

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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md13.4 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Data Loading

Setup

Basic loader returning data, consumed via useLoaderData:

// 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:

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:

// 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
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:

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  staleTime: Infinity,
})

Globally:

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.

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:

// 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:

// 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:

// 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:

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:

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:

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:

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):

await router.invalidate({ sync: true })

Error Handling

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
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.

// 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:

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:

// 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:

// 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:

// 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:

// 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),
})

HIGH: Returning entire search object from loaderDeps

// WRONG — loader re-runs on ANY search param change
loaderDeps: ({ search }) => search

// CORRECT — only re-run when page changes
loaderDeps: ({ search }) => ({ page: search.page })

Returning the whole search object means unrelated param changes (e.g., sortDirection, viewMode) trigger unnecessary reloads because deep equality fails on the entire object.

Tensions

  • Client-first loaders vs SSR expectations: Loaders run on the client by default. When using SSR (TanStack Start), they run on both client and server. Browser-only APIs work by default but break under SSR. Server-only APIs (fs, db) break by default but work under Start server functions. See router-core-ssr/SKILL.md.
  • Built-in SWR cache vs external cache coordination: Router has built-in caching. When using TanStack Query, set defaultPreloadStaleTime: 0 to avoid double-caching. See compositions-router-query/SKILL.md.

Cross-References

  • See also: router-core-search-params/SKILL.md — loaderDeps consumes validated search params as cache keys
  • See also: compositions-router-query/SKILL.md — for external cache coordination with TanStack Query

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Build AI chat interfaces using ai-elements components — conversations, messages, tool displays, prompt inputs, and more. Use when the user wants to build a chatbot, AI assistant UI, or any AI-powered chat interface.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

hashintel/brunch82026년 10월 17일 업데이트

Search the live web via Perplexity Search API. Use when you need current documentation, release notes, vendor pages, news, domain-constrained web search, or date/recency filtering. Not for local codebase search or stable docs already in context.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

hashintel/brunch82026년 10월 17일 업데이트

cli-cdp

무료

Chrome DevTools CLI for browser automation via shell commands. Use when interacting with web pages from the command line — navigating, clicking, filling forms, inspecting console/network, taking screenshots, or extracting page content. Triggers on: browse a page, automate Chrome, inspect console, check network requests, take a screenshot, fill a form, click a button.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

hashintel/brunch82026년 10월 17일 업데이트

Uses the chrome-devtools-axi CLI for browser automation, accessibility-tree snapshots, console and network inspection, screenshots, Lighthouse audits, and performance traces. Use when interacting with Chrome from the shell, especially when the user mentions chrome-devtools-axi, AX snapshots, browser debugging, or DevTools automation from the command line.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

hashintel/brunch82026년 10월 17일 업데이트

cli-cmux

무료

Deep expertise in cmux — the terminal multiplexer with native browser views. Use when managing panes, reading terminal output, sending keystrokes, opening browser views, or manually testing web UIs and TUIs inside cmux. Triggers on: cmux, open a browser pane, split terminal, read screen, send keys, test this UI in cmux, preview in cmux.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

hashintel/brunch82026년 10월 17일 업데이트

Uses the gh-axi CLI for GitHub shell operations: issue, pull request, workflow run, release, repo, search, and API tasks. Prefer this over regular `gh` for GitHub reads and simple mutations when an agent needs compact, structured, suggestion-rich output. Triggers on: gh, GitHub CLI, github issue, github pr, pull request, workflow run, github release, gh api, repo inspection, list PRs, view issue, check workflow runs, inspect repo, GitHub shell operations.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

hashintel/brunch82026년 10월 17일 업데이트

hashintel의 스킬 모두 보기

이 스킬의 문제 신고