Files
dotfiles/dot_agents/skills/tanstack-router-best-practices/rules/load-use-loaders.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

3.8 KiB

load-use-loaders: Use Route Loaders for Data Fetching

Priority: HIGH

Explanation

Route loaders execute before the route renders, enabling data to be ready when the component mounts. This prevents loading waterfalls, enables preloading, and integrates with the router's caching layer.

Bad Example

// Fetching in component - creates waterfall
function PostsPage() {
  const [posts, setPosts] = useState<Post[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Route renders, THEN data fetches, THEN UI updates
    fetchPosts().then((data) => {
      setPosts(data)
      setLoading(false)
    })
  }, [])

  if (loading) return <Loading />
  return <PostList posts={posts} />
}

// No preloading possible - user sees loading state on navigation

Good Example

// routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await fetchPosts()
    return { posts }
  },
  component: PostsPage,
})

function PostsPage() {
  // Data is ready when component mounts - no loading state needed
  const { posts } = Route.useLoaderData()
  return <PostList posts={posts} />
}

Good Example: With Parameters

// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    // params are type-safe and guaranteed to exist
    const post = await fetchPost(params.postId)
    const comments = await fetchComments(params.postId)
    return { post, comments }
  },
  component: PostDetailPage,
})

function PostDetailPage() {
  const { post, comments } = Route.useLoaderData()
  const { postId } = Route.useParams()

  return (
    <article>
      <h1>{post.title}</h1>
      <PostContent content={post.content} />
      <CommentList comments={comments} />
    </article>
  )
}

Good Example: With TanStack Query

// routes/posts/$postId.tsx
import { queryOptions } from '@tanstack/react-query'

const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ['posts', postId],
    queryFn: () => fetchPost(postId),
  })

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, context: { queryClient } }) => {
    // Ensure data is in cache before render
    await queryClient.ensureQueryData(postQueryOptions(params.postId))
  },
  component: PostDetailPage,
})

function PostDetailPage() {
  const { postId } = Route.useParams()
  // useSuspenseQuery because loader guarantees data exists
  const { data: post } = useSuspenseQuery(postQueryOptions(postId))

  return <PostContent post={post} />
}

Loader Context Properties

export const Route = createFileRoute('/posts')({
  loader: async ({
    params,       // Route path parameters
    context,      // Route context (queryClient, auth, etc.)
    abortController, // For cancelling stale requests
    cause,        // 'enter' | 'preload' | 'stay'
    deps,         // Dependencies from loaderDeps
    preload,      // Boolean: true if preloading
  }) => {
    // Use abortController for fetch cancellation
    const response = await fetch('/api/posts', {
      signal: abortController.signal,
    })

    // Different behavior for preload vs navigation
    if (preload) {
      // Lighter data for preload
      return { posts: await response.json() }
    }

    // Full data for actual navigation
    const posts = await response.json()
    const stats = await fetchStats()
    return { posts, stats }
  },
})

Context

  • Loaders run during route matching, before component render
  • Supports parallel loading across nested routes
  • Enables preloading on link hover/focus
  • Built-in stale-while-revalidate caching
  • For complex caching needs, integrate with TanStack Query
  • Use beforeLoad for auth checks and redirects