
Tanstack Router Best Practices
Wire TanStack Router with typed root context so loaders and routes use injected query clients and auth instead of global imports.
Overview
tanstack-router-best-practices is an agent skill for the Build phase that teaches typed root context with createRootRouteWithContext so TanStack Router loaders use injected dependencies instead of globals.
Install
npx skills add https://github.com/deckardger/tanstack-agent-skills --skill tanstack-router-best-practicesWhat is this skill?
- Recommends createRootRouteWithContext for a typed RouterContext across the route tree
- Shows injecting queryClient and auth state at router creation instead of per-file global imports
- Demonstrates loaders using context for easier testing and looser coupling
- Contrasts bad global-import loaders with context-aware ensureQueryData patterns
- Covers __root layout with Outlet plus shared chrome (header/footer) pattern
- Documents ctx-root-context rule with explicit bad vs good route examples
Adoption & trust: 4.1k installs on skills.sh; 180 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your TanStack Router routes import query clients and auth from globals, making loaders hard to test and tightly coupled.
Who is it for?
Solo builders shipping React SPAs or dashboards on TanStack Router who plan to use loaders with React Query or auth state.
Skip if: Projects on Next.js App Router only, or tiny prototypes with a single route and no shared loader dependencies.
When should I use this skill?
Scaffolding or refactoring TanStack Router file routes where loaders need queryClient, auth, or other shared services without global imports.
What do I get? / Deliverables
You get a root route context interface, router setup that supplies it, and route loaders that read dependencies from context.
- Typed RouterContext interface
- Root route and router factory wiring context into createRouter
Recommended Skills
Journey fit
Router architecture and context typing are decided while building the React app shell and route tree, before ship-time perf tuning. TanStack Router file routes, loaders, and root context are frontend routing and data-loading concerns.
How it compares
Opinionated TanStack Router structure skill, not a generic React routing tutorial or an MCP data-fetch server.
Common Questions / FAQ
Who is tanstack-router-best-practices for?
Indie developers and small teams using TanStack Router with file-based routes who want context-based dependency injection.
When should I use tanstack-router-best-practices?
During Build when creating __root.tsx, router.tsx, or route loaders where you would otherwise import queryClient or auth globals.
Is tanstack-router-best-practices safe to install?
Review the Security Audits panel on this Prism page and your agent permissions; the skill is documentation-style and does not imply extra network access by itself.
SKILL.md
READMESKILL.md - Tanstack Router Best Practices
# 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 ```tsx // 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 ```tsx // 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 ```tsx // 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 ```tsx // 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 | |