public-rules › TanStack Router techs/tanstack-router

Provide shared dependencies through typed router context

HIGH1.0.0
When to apply Before planning, writing, changing, or reviewing how TanStack Router loaders and beforeLoad functions access shared clients and services, such as a QueryClient, authentication state, or an API client, including SSR setup and tests.

Declare the dependencies routes share, such as the QueryClient and authentication state, as the root route's context type with createRootRouteWithContext, and pass instances when creating the router. Read them from context in loaders and beforeLoad, instead of importing module-level singletons.

Implementation

  • Define a RouterContext interface and create the root route with createRootRouteWithContext<RouterContext>().
  • Create the router in a function, such as getRouter(), that builds a new QueryClient and other request-specific dependencies and passes them in createRouter({ context }).
  • With server rendering and TanStack Query, call setupRouterSsrQueryIntegration({ router, queryClient }) from @tanstack/react-router-ssr-query to wire dehydration, hydration, and the provider.
  • Extend context for a subtree by returning values from beforeLoad, such as the authenticated user for protected routes.
  • In tests, create the router with fresh dependencies and a memory history, so each test gets isolated state.

Rationale

On a server, a module-level QueryClient is shared by every request the process handles, so one user's cached data can appear in another's response. Context created per router instance keeps each request isolated, gives loaders typed access to their dependencies, and lets tests provide their own.

Examples

Incorrect (counterexample):

// lib/query-client.ts
export const queryClient = new QueryClient();

// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: () => queryClient.ensureQueryData(postQueries.list()),
});

Every server request shares one cache, and tests cannot replace the client.

Correct:

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

export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootComponent,
});
// router.tsx
export function getRouter() {
  const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000 } } });
  const router = createRouter({ routeTree, context: { queryClient }, defaultPreloadStaleTime: 0 });
  setupRouterSsrQueryIntegration({ router, queryClient });
  return router;
}
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(postQueries.list()),
});

Validation

Search route files for imports of shared clients, such as a module-level queryClient, and check that loaders use context instead. Check that server rendering creates a new router, with new dependencies, per request.

A module-level import of a stateless utility, such as a pure formatting function, is not a violation.