Files
findyourpilot/.agent/skills/tanstack-router-best-practices/rules/ctx-root-context.md
2026-03-02 21:16:26 +01:00

4.4 KiB

ctx-root-context: Define Context at Root Route

Priority: LOW

Explanation

Use createRootRouteWithContext to define typed context that flows through your entire route tree. This enables dependency injection for things like query clients, auth state, and services.

Bad Example

// No context - importing globals directly
// routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
import { queryClient } from '@/lib/query-client'  // Global import

export const Route = createRootRoute({
  component: RootComponent,
})

// routes/posts.tsx
import { queryClient } from '@/lib/query-client'  // Import again

export const Route = createFileRoute('/posts')({
  loader: async () => {
    // Using global - harder to test, couples to implementation
    return queryClient.ensureQueryData(postQueries.list())
  },
})

Good Example

// routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'

// Define the context interface
interface RouterContext {
  queryClient: QueryClient
  auth: {
    user: User | null
    isAuthenticated: boolean
  }
}

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootComponent,
})

function RootComponent() {
  return (
    <>
      <Header />
      <main>
        <Outlet />
      </main>
      <Footer />
    </>
  )
}

// router.tsx - Provide context when creating router
import { createRouter } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'

export function getRouter(auth: RouterContext['auth'] = { user: null, isAuthenticated: false }) {
  const queryClient = new QueryClient()

  const router = createRouter({
    routeTree,
    context: {
      queryClient,
      auth,
    },
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0,
    scrollRestoration: true,
  })

  setupRouterSsrQueryIntegration({ router, queryClient })

  return router
}

// routes/posts.tsx - Use context in loaders
export const Route = createFileRoute('/posts')({
  loader: async ({ context: { queryClient } }) => {
    // Context is typed and injected
    return queryClient.ensureQueryData(postQueries.list())
  },
})

Good Example: Auth-Protected Routes

// routes/__root.tsx
interface RouterContext {
  queryClient: QueryClient
  auth: AuthState
}

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootComponent,
})

// routes/_authenticated.tsx - Layout route for protected pages
export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
  component: AuthenticatedLayout,
})

// routes/_authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
  loader: async ({ context: { queryClient, auth } }) => {
    // We know user is authenticated from parent beforeLoad
    return queryClient.ensureQueryData(
      dashboardQueries.forUser(auth.user!.id)
    )
  },
})

Extending Context with beforeLoad

// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  beforeLoad: async ({ context, params }) => {
    // Extend context with route-specific data
    const post = await fetchPost(params.postId)

    return {
      post,  // Available to this route and children
    }
  },
  loader: async ({ context }) => {
    // context now includes 'post' from beforeLoad
    const comments = await fetchComments(context.post.id)
    return { comments }
  },
})

Context vs. Loader Data

Context Loader Data
Available in beforeLoad, loader, and component Only available in component
Set at router creation or in beforeLoad Returned from loader
Good for services, clients, auth Good for route-specific data
Flows down to all children Specific to route

Context

  • Type the context interface in createRootRouteWithContext<T>()
  • Provide context when calling createRouter({ context: {...} })
  • Context flows from root to all nested routes
  • Use beforeLoad to extend context for specific subtrees
  • Enables testability - inject mocks in tests
  • Avoids global imports and singletons