Provide shared dependencies through typed router context
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
RouterContextinterface and create the root route withcreateRootRouteWithContext<RouterContext>(). - Create the router in a function, such as
getRouter(), that builds a newQueryClientand other request-specific dependencies and passes them increateRouter({ context }). - With server rendering and TanStack Query, call
setupRouterSsrQueryIntegration({ router, queryClient })from@tanstack/react-router-ssr-queryto 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.