TanStack Router Best Practices Comprehensive guidelines for implementing TanStack Router patterns in React applications. These rules optimize type safety, data loading, navigation, and code organization. When to Apply - Setting up application routing - Creating new routes and layouts - Implementing search parameter handling - Configuring data loaders - Setting up code splitting - Integrating with TanStack Query - Refactoring navigation patterns Rule Categories by Priority | Priority | Category | Rules | Impact | |----------|----------|-------|--------| | CRITICAL | Type Safety | 4 rules | Pre…

)({\n component: CatchAllNotFound,\n})\n\nfunction CatchAllNotFound() {\n const { _splat } = Route.useParams()\n\n return (\n \u003cdiv>\n \u003ch1>Page Not Found\u003c/h1>\n \u003cp>No page exists at: /{_splat}\u003c/p>\n \u003cLink to=\"/\">Go to homepage\u003c/Link>\n \u003c/div>\n )\n}\n```\n\n## Good Example: Nested Not Found Bubbling\n\n```tsx\n// Not found bubbles up through route tree\n// routes/posts.tsx\nexport const Route = createFileRoute('/posts')({\n notFoundComponent: PostsNotFound, // Catches child 404s too\n})\n\n// routes/posts/$postId.tsx\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params }) => {\n const post = await fetchPost(params.postId)\n if (!post) throw notFound()\n return post\n },\n // No notFoundComponent - bubbles to parent\n})\n\n// routes/posts/$postId/comments.tsx\nexport const Route = createFileRoute('/posts/$postId/comments')({\n loader: async ({ params }) => {\n const comments = await fetchComments(params.postId)\n if (!comments) throw notFound() // Bubbles to /posts notFoundComponent\n return comments\n },\n})\n```\n\n## Context\n\n- `notFound()` throws a special error caught by nearest `notFoundComponent`\n- Not found bubbles up the route tree if not handled locally\n- Use `defaultNotFoundComponent` on router for global fallback\n- Pass data to `notFound({ data })` for contextual 404 pages\n- Catch-all routes (`/ tanstack-router-best-practices — Skillopedia ) can handle truly unknown paths\n- Different from error boundaries - specifically for 404 cases\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4679,"content_sha256":"44628dbd8baa8e25fde9fe3a55f478d9cac9afb7a4bdd5c1e834a85a658daa60"},{"filename":"rules/load-ensure-query-data.md","content":"# load-ensure-query-data: Use ensureQueryData with TanStack Query\n\n## Priority: HIGH\n\n## Explanation\n\nWhen integrating TanStack Router with TanStack Query, use `queryClient.ensureQueryData()` in loaders instead of `prefetchQuery()`. This respects the cache, awaits data if missing, and returns the data for potential use.\n\n## Bad Example\n\n```tsx\n// Using prefetchQuery - doesn't return data, can't await stale check\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params, context: { queryClient } }) => {\n // prefetchQuery never throws, swallows errors\n queryClient.prefetchQuery({\n queryKey: ['posts', params.postId],\n queryFn: () => fetchPost(params.postId),\n })\n // No await - might not complete before render\n // No return value to use\n },\n})\n\n// Fetching directly - bypasses TanStack Query cache\nexport const Route = createFileRoute('/posts')({\n loader: async () => {\n const posts = await fetchPosts() // Not cached\n return { posts }\n },\n})\n```\n\n## Good Example\n\n```tsx\n// Define queryOptions for reuse\nconst postQueryOptions = (postId: string) =>\n queryOptions({\n queryKey: ['posts', postId],\n queryFn: () => fetchPost(postId),\n staleTime: 5 * 60 * 1000, // 5 minutes\n })\n\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params, context: { queryClient } }) => {\n // ensureQueryData:\n // - Returns cached data if fresh\n // - Fetches and caches if missing or stale\n // - Awaits completion\n // - Throws on error (caught by error boundary)\n await queryClient.ensureQueryData(postQueryOptions(params.postId))\n },\n component: PostPage,\n})\n\nfunction PostPage() {\n const { postId } = Route.useParams()\n\n // Data guaranteed to exist from loader\n const { data: post } = useSuspenseQuery(postQueryOptions(postId))\n\n return \u003cPostContent post={post} />\n}\n```\n\n## Good Example: Multiple Parallel Queries\n\n```tsx\nexport const Route = createFileRoute('/dashboard')({\n loader: async ({ context: { queryClient } }) => {\n // Parallel data fetching\n await Promise.all([\n queryClient.ensureQueryData(statsQueries.overview()),\n queryClient.ensureQueryData(activityQueries.recent()),\n queryClient.ensureQueryData(notificationQueries.unread()),\n ])\n },\n})\n```\n\n## Good Example: Dependent Queries\n\n```tsx\nexport const Route = createFileRoute('/users/$userId/posts')({\n loader: async ({ params, context: { queryClient } }) => {\n // First query needed for second\n const user = await queryClient.ensureQueryData(\n userQueries.detail(params.userId)\n )\n\n // Dependent query uses result\n await queryClient.ensureQueryData(\n postQueries.byAuthor(user.id)\n )\n },\n})\n```\n\n## Router Configuration for TanStack Query\n\n```tsx\n// router.tsx\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 60 * 1000, // 1 minute default\n },\n },\n})\n\nexport const router = createRouter({\n routeTree,\n context: { queryClient },\n\n // Let TanStack Query manage caching\n defaultPreloadStaleTime: 0,\n\n // SSR: Dehydrate query cache\n dehydrate: () => ({\n queryClientState: dehydrate(queryClient),\n }),\n\n // SSR: Hydrate on client\n hydrate: (dehydrated) => {\n hydrate(queryClient, dehydrated.queryClientState)\n },\n\n // Wrap with QueryClientProvider\n Wrap: ({ children }) => (\n \u003cQueryClientProvider client={queryClient}>\n {children}\n \u003c/QueryClientProvider>\n ),\n})\n```\n\n## ensureQueryData vs prefetchQuery vs fetchQuery\n\n| Method | Returns | Throws | Awaits | Use Case |\n|--------|---------|--------|--------|----------|\n| `ensureQueryData` | Data | Yes | Yes | Route loaders (recommended) |\n| `prefetchQuery` | void | No | Yes | Background prefetching |\n| `fetchQuery` | Data | Yes | Yes | When you need data immediately |\n\n## Context\n\n- `ensureQueryData` is the recommended method for route loaders\n- Respects `staleTime` - won't refetch fresh cached data\n- Errors propagate to route error boundaries\n- Use `queryOptions()` factory for type-safe, reusable query definitions\n- Set `defaultPreloadStaleTime: 0` to let TanStack Query manage cache\n- Pair with `useSuspenseQuery` in components for guaranteed data\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4211,"content_sha256":"6cb40748f915a64058dbcaf33aadf080f7a2b9324b2c6e4c1e127dcb71259edf"},{"filename":"rules/load-parallel.md","content":"# load-parallel: Leverage Parallel Route Loading\n\n## Priority: MEDIUM\n\n## Explanation\n\nTanStack Router loads nested route data in parallel, not sequentially. Structure your routes and loaders to maximize parallelization and avoid creating artificial waterfalls.\n\n## Bad Example\n\n```tsx\n// Creating waterfall with dependent beforeLoad\nexport const Route = createFileRoute('/dashboard')({\n beforeLoad: async () => {\n const user = await fetchUser() // 200ms\n const permissions = await fetchPermissions(user.id) // 200ms\n const preferences = await fetchPreferences(user.id) // 200ms\n // Total: 600ms (sequential)\n\n return { user, permissions, preferences }\n },\n})\n\n// Or nesting data dependencies incorrectly\n// routes/posts.tsx\nexport const Route = createFileRoute('/posts')({\n loader: async () => {\n const posts = await fetchPosts() // 300ms\n return { posts }\n },\n})\n\n// routes/posts/$postId.tsx\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params }) => {\n // Waits for parent to complete first - waterfall!\n const post = await fetchPost(params.postId) // +200ms\n return { post }\n },\n})\n```\n\n## Good Example: Parallel in Single Loader\n\n```tsx\nexport const Route = createFileRoute('/dashboard')({\n beforeLoad: async () => {\n // All requests start simultaneously\n const [user, config] = await Promise.all([\n fetchUser(), // 200ms\n fetchAppConfig(), // 150ms\n ])\n // Total: 200ms (parallel)\n\n return { user, config }\n },\n loader: async ({ context }) => {\n // These also run in parallel with each other\n const [stats, activity, notifications] = await Promise.all([\n fetchDashboardStats(context.user.id),\n fetchRecentActivity(context.user.id),\n fetchNotifications(context.user.id),\n ])\n\n return { stats, activity, notifications }\n },\n})\n```\n\n## Good Example: Parallel Nested Routes\n\n```tsx\n// Parent and child loaders run in PARALLEL\n// routes/posts.tsx\nexport const Route = createFileRoute('/posts')({\n loader: async () => {\n // This runs...\n const categories = await fetchCategories()\n return { categories }\n },\n})\n\n// routes/posts/$postId.tsx\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params }) => {\n // ...at the SAME TIME as this!\n const post = await fetchPost(params.postId)\n const comments = await fetchComments(params.postId)\n return { post, comments }\n },\n})\n\n// Navigation to /posts/123:\n// - Both loaders start simultaneously\n// - Total time = max(categoriesTime, postTime + commentsTime)\n// - NOT categoriesTime + postTime + commentsTime\n```\n\n## Good Example: With TanStack Query\n\n```tsx\n// routes/posts.tsx\nexport const Route = createFileRoute('/posts')({\n loader: async ({ context: { queryClient } }) => {\n // These all start in parallel\n await Promise.all([\n queryClient.ensureQueryData(postQueries.list()),\n queryClient.ensureQueryData(categoryQueries.all()),\n ])\n },\n})\n\n// routes/posts/$postId.tsx\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params, context: { queryClient } }) => {\n // Runs in parallel with parent loader\n await Promise.all([\n queryClient.ensureQueryData(postQueries.detail(params.postId)),\n queryClient.ensureQueryData(commentQueries.forPost(params.postId)),\n ])\n },\n})\n```\n\n## Good Example: Streaming Non-Critical Data\n\n```tsx\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params, context: { queryClient } }) => {\n // Critical data - await\n const post = await queryClient.ensureQueryData(\n postQueries.detail(params.postId)\n )\n\n // Non-critical - start but don't await (stream in later)\n queryClient.prefetchQuery(commentQueries.forPost(params.postId))\n queryClient.prefetchQuery(relatedQueries.forPost(params.postId))\n\n return { post }\n },\n component: PostPage,\n})\n\nfunction PostPage() {\n const { post } = Route.useLoaderData()\n const { postId } = Route.useParams()\n\n // Critical data ready immediately\n // Non-critical loads in component with loading state\n const { data: comments, isLoading } = useQuery(\n commentQueries.forPost(postId)\n )\n\n return (\n \u003carticle>\n \u003cPostContent post={post} />\n {isLoading ? \u003cCommentsSkeleton /> : \u003cComments data={comments} />}\n \u003c/article>\n )\n}\n```\n\n## Route Loading Timeline\n\n```\nNavigation to /posts/123\n\nWithout parallelization:\n├─ beforeLoad (parent) ████████\n├─ loader (parent) ████████\n├─ beforeLoad (child) ████\n├─ loader (child) ████████\n└─ Render █\n\nWith parallelization:\n├─ beforeLoad (parent) ████████\n├─ beforeLoad (child) ████\n├─ loader (parent) ████████\n├─ loader (child) ████████████\n└─ Render █\n```\n\n## Context\n\n- Nested route loaders run in parallel by default\n- `beforeLoad` runs before `loader` (for auth, context setup)\n- Use `Promise.all` for parallel fetches within a single loader\n- Parent context is available in child loaders (after beforeLoad)\n- Prefetch non-critical data without awaiting for streaming\n- Monitor network tab to verify parallelization\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5408,"content_sha256":"9f9357380d0df5a1a3c3da1b8c5f15ad056842a436467be3ab33e097612f0185"},{"filename":"rules/load-use-loaders.md","content":"# load-use-loaders: Use Route Loaders for Data Fetching\n\n## Priority: HIGH\n\n## Explanation\n\nRoute 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.\n\n## Bad Example\n\n```tsx\n// Fetching in component - creates waterfall\nfunction PostsPage() {\n const [posts, setPosts] = useState\u003cPost[]>([])\n const [loading, setLoading] = useState(true)\n\n useEffect(() => {\n // Route renders, THEN data fetches, THEN UI updates\n fetchPosts().then((data) => {\n setPosts(data)\n setLoading(false)\n })\n }, [])\n\n if (loading) return \u003cLoading />\n return \u003cPostList posts={posts} />\n}\n\n// No preloading possible - user sees loading state on navigation\n```\n\n## Good Example\n\n```tsx\n// routes/posts.tsx\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/posts')({\n loader: async () => {\n const posts = await fetchPosts()\n return { posts }\n },\n component: PostsPage,\n})\n\nfunction PostsPage() {\n // Data is ready when component mounts - no loading state needed\n const { posts } = Route.useLoaderData()\n return \u003cPostList posts={posts} />\n}\n```\n\n## Good Example: With Parameters\n\n```tsx\n// routes/posts/$postId.tsx\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params }) => {\n // params are type-safe and guaranteed to exist\n const post = await fetchPost(params.postId)\n const comments = await fetchComments(params.postId)\n return { post, comments }\n },\n component: PostDetailPage,\n})\n\nfunction PostDetailPage() {\n const { post, comments } = Route.useLoaderData()\n const { postId } = Route.useParams()\n\n return (\n \u003carticle>\n \u003ch1>{post.title}\u003c/h1>\n \u003cPostContent content={post.content} />\n \u003cCommentList comments={comments} />\n \u003c/article>\n )\n}\n```\n\n## Good Example: With TanStack Query\n\n```tsx\n// routes/posts/$postId.tsx\nimport { queryOptions } from '@tanstack/react-query'\n\nconst postQueryOptions = (postId: string) =>\n queryOptions({\n queryKey: ['posts', postId],\n queryFn: () => fetchPost(postId),\n })\n\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params, context: { queryClient } }) => {\n // Ensure data is in cache before render\n await queryClient.ensureQueryData(postQueryOptions(params.postId))\n },\n component: PostDetailPage,\n})\n\nfunction PostDetailPage() {\n const { postId } = Route.useParams()\n // useSuspenseQuery because loader guarantees data exists\n const { data: post } = useSuspenseQuery(postQueryOptions(postId))\n\n return \u003cPostContent post={post} />\n}\n```\n\n## Loader Context Properties\n\n```tsx\nexport const Route = createFileRoute('/posts')({\n loader: async ({\n params, // Route path parameters\n context, // Route context (queryClient, auth, etc.)\n abortController, // For cancelling stale requests\n cause, // 'enter' | 'preload' | 'stay'\n deps, // Dependencies from loaderDeps\n preload, // Boolean: true if preloading\n }) => {\n // Use abortController for fetch cancellation\n const response = await fetch('/api/posts', {\n signal: abortController.signal,\n })\n\n // Different behavior for preload vs navigation\n if (preload) {\n // Lighter data for preload\n return { posts: await response.json() }\n }\n\n // Full data for actual navigation\n const posts = await response.json()\n const stats = await fetchStats()\n return { posts, stats }\n },\n})\n```\n\n## Context\n\n- Loaders run during route matching, before component render\n- Supports parallel loading across nested routes\n- Enables preloading on link hover/focus\n- Built-in stale-while-revalidate caching\n- For complex caching needs, integrate with TanStack Query\n- Use `beforeLoad` for auth checks and redirects\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3883,"content_sha256":"2804995a36ab6fbdee628ce73c9c72148b8c010117620adcbfeb9bb163366fd7"},{"filename":"rules/nav-link-component.md","content":"# nav-link-component: Prefer Link Component for Navigation\n\n## Priority: MEDIUM\n\n## Explanation\n\nUse the `\u003cLink>` component for navigation instead of `useNavigate()` when possible. Links render proper `\u003ca>` tags with valid `href` attributes, enabling right-click → open in new tab, better SEO, and accessibility.\n\n## Bad Example\n\n```tsx\n// Using onClick with navigate - loses standard link behavior\nfunction PostCard({ post }: { post: Post }) {\n const navigate = useNavigate()\n\n return (\n \u003cdiv\n onClick={() => navigate({ to: '/posts/$postId', params: { postId: post.id } })}\n className=\"post-card\"\n >\n \u003ch2>{post.title}\u003c/h2>\n \u003cp>{post.excerpt}\u003c/p>\n \u003c/div>\n )\n}\n// Problems:\n// - No right-click → open in new tab\n// - No cmd/ctrl+click for new tab\n// - Not announced as link to screen readers\n// - No valid href for SEO\n```\n\n## Good Example\n\n```tsx\nimport { Link } from '@tanstack/react-router'\n\nfunction PostCard({ post }: { post: Post }) {\n return (\n \u003cLink\n to=\"/posts/$postId\"\n params={{ postId: post.id }}\n className=\"post-card\"\n >\n \u003ch2>{post.title}\u003c/h2>\n \u003cp>{post.excerpt}\u003c/p>\n \u003c/Link>\n )\n}\n// Benefits:\n// - Renders \u003ca href=\"/posts/123\">\n// - Right-click menu works\n// - Cmd/Ctrl+click opens new tab\n// - Screen readers announce as link\n// - Preloading works on hover\n```\n\n## Good Example: With Search Params\n\n```tsx\nfunction FilteredLink() {\n return (\n \u003cLink\n to=\"/products\"\n search={{ category: 'electronics', sort: 'price' }}\n >\n View Electronics\n \u003c/Link>\n )\n}\n\n// Preserving existing search params\nfunction SortLink({ sort }: { sort: 'asc' | 'desc' }) {\n return (\n \u003cLink\n to=\".\" // Current route\n search={(prev) => ({ ...prev, sort })}\n >\n Sort {sort === 'asc' ? 'Ascending' : 'Descending'}\n \u003c/Link>\n )\n}\n```\n\n## Good Example: With Active States\n\n```tsx\nfunction NavLink({ to, children }: { to: string; children: React.ReactNode }) {\n return (\n \u003cLink\n to={to}\n activeProps={{\n className: 'nav-link-active',\n 'aria-current': 'page',\n }}\n inactiveProps={{\n className: 'nav-link',\n }}\n activeOptions={{\n exact: true, // Only active on exact match\n }}\n >\n {children}\n \u003c/Link>\n )\n}\n\n// Or use render props for more control\nfunction CustomNavLink({ to, children }: { to: string; children: React.ReactNode }) {\n return (\n \u003cLink to={to}>\n {({ isActive }) => (\n \u003cspan className={isActive ? 'text-blue-600 font-bold' : 'text-gray-600'}>\n {children}\n {isActive && \u003cCheckIcon className=\"ml-2\" />}\n \u003c/span>\n )}\n \u003c/Link>\n )\n}\n```\n\n## Good Example: With Preloading\n\n```tsx\nfunction PostList({ posts }: { posts: Post[] }) {\n return (\n \u003cul>\n {posts.map(post => (\n \u003cli key={post.id}>\n \u003cLink\n to=\"/posts/$postId\"\n params={{ postId: post.id }}\n preload=\"intent\" // Preload on hover/focus\n preloadDelay={100} // Wait 100ms before preloading\n >\n {post.title}\n \u003c/Link>\n \u003c/li>\n ))}\n \u003c/ul>\n )\n}\n```\n\n## When to Use useNavigate Instead\n\n```tsx\n// 1. After form submission\nconst createPost = useMutation({\n mutationFn: submitPost,\n onSuccess: (data) => {\n navigate({ to: '/posts/$postId', params: { postId: data.id } })\n },\n})\n\n// 2. After authentication\nasync function handleLogin(credentials: Credentials) {\n await login(credentials)\n navigate({ to: '/dashboard' })\n}\n\n// 3. Programmatic redirects\nuseEffect(() => {\n if (!isAuthenticated) {\n navigate({ to: '/login', search: { redirect: location.pathname } })\n }\n}, [isAuthenticated])\n```\n\n## Context\n\n- `\u003cLink>` renders actual `\u003ca>` tags with proper `href`\n- Supports all standard link behaviors (middle-click, cmd+click, etc.)\n- Enables preloading on hover/focus\n- Better for SEO - crawlers can follow links\n- Reserve `useNavigate` for side effects and programmatic navigation\n- Use `\u003cNavigate>` component for immediate redirects on render\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4065,"content_sha256":"a8ed634f01d280e46c8552020e4abab1fa2ad53655329bb71648cfa956f287d1"},{"filename":"rules/nav-route-masks.md","content":"# nav-route-masks: Use Route Masks for Modal URLs\n\n## Priority: LOW\n\n## Explanation\n\nRoute masks let you display one URL while internally routing to another. This is useful for modals, sheets, and overlays where you want a shareable URL that shows the modal, but navigating there directly should show the full page.\n\n## Bad Example\n\n```tsx\n// Modal without proper URL handling\nfunction PostList() {\n const [selectedPost, setSelectedPost] = useState\u003cstring | null>(null)\n\n return (\n \u003cdiv>\n {posts.map(post => (\n \u003cdiv key={post.id} onClick={() => setSelectedPost(post.id)}>\n {post.title}\n \u003c/div>\n ))}\n\n {selectedPost && (\n \u003cModal onClose={() => setSelectedPost(null)}>\n \u003cPostDetail postId={selectedPost} />\n \u003c/Modal>\n )}\n \u003c/div>\n )\n}\n\n// Problems:\n// - URL doesn't change when modal opens\n// - Can't share link to modal\n// - Back button doesn't close modal\n// - Refresh loses modal state\n```\n\n## Good Example: Route Masks for Modal\n\n```tsx\n// routes/posts.tsx\nexport const Route = createFileRoute('/posts')({\n component: PostList,\n})\n\nfunction PostList() {\n const posts = usePosts()\n\n return (\n \u003cdiv>\n {posts.map(post => (\n \u003cLink\n key={post.id}\n to=\"/posts/$postId\"\n params={{ postId: post.id }}\n mask={{\n to: '/posts',\n // URL shows /posts but routes to /posts/$postId\n }}\n >\n {post.title}\n \u003c/Link>\n ))}\n \u003cOutlet /> {/* Modal renders here */}\n \u003c/div>\n )\n}\n\n// routes/posts/$postId.tsx\nexport const Route = createFileRoute('/posts/$postId')({\n component: PostModal,\n})\n\nfunction PostModal() {\n const { postId } = Route.useParams()\n const navigate = useNavigate()\n\n return (\n \u003cModal onClose={() => navigate({ to: '/posts' })}>\n \u003cPostDetail postId={postId} />\n \u003c/Modal>\n )\n}\n\n// User clicks post:\n// - URL stays /posts (masked)\n// - PostModal renders\n// - Share link goes to /posts/$postId (real URL)\n// - Direct navigation to /posts/$postId shows full page (no mask)\n```\n\n## Good Example: With Search Params\n\n```tsx\nfunction PostList() {\n return (\n \u003cdiv>\n {posts.map(post => (\n \u003cLink\n key={post.id}\n to=\"/posts/$postId\"\n params={{ postId: post.id }}\n mask={{\n to: '/posts',\n search: { modal: post.id }, // /posts?modal=123\n }}\n >\n {post.title}\n \u003c/Link>\n ))}\n \u003c/div>\n )\n}\n```\n\n## Good Example: Programmatic Navigation with Mask\n\n```tsx\nfunction PostCard({ post }: { post: Post }) {\n const navigate = useNavigate()\n\n const openInModal = () => {\n navigate({\n to: '/posts/$postId',\n params: { postId: post.id },\n mask: {\n to: '/posts',\n },\n })\n }\n\n const openFullPage = () => {\n navigate({\n to: '/posts/$postId',\n params: { postId: post.id },\n // No mask - shows real URL\n })\n }\n\n return (\n \u003cdiv>\n \u003ch3>{post.title}\u003c/h3>\n \u003cbutton onClick={openInModal}>Quick View\u003c/button>\n \u003cbutton onClick={openFullPage}>Full Page\u003c/button>\n \u003c/div>\n )\n}\n```\n\n## Good Example: Unmask on Interaction\n\n```tsx\nfunction PostModal() {\n const { postId } = Route.useParams()\n const navigate = useNavigate()\n\n const expandToFullPage = () => {\n // Navigate to real URL, removing mask\n navigate({\n to: '/posts/$postId',\n params: { postId },\n // No mask = real URL\n replace: true, // Replace history entry\n })\n }\n\n return (\n \u003cModal>\n \u003cPostDetail postId={postId} />\n \u003cbutton onClick={expandToFullPage}>\n Expand to full page\n \u003c/button>\n \u003c/Modal>\n )\n}\n```\n\n## Route Mask Behavior\n\n| Scenario | URL Shown | Actual Route |\n|----------|-----------|--------------|\n| Click masked link | Masked URL | Real route |\n| Share/copy URL | Real URL | Real route |\n| Direct navigation | Real URL | Real route |\n| Browser refresh | Depends on URL in bar | Matches URL |\n| Back button | Previous URL | Previous route |\n\n## Context\n\n- Masks are client-side only - shared URLs are the real route\n- Direct navigation to real URL bypasses mask (shows full page)\n- Back button navigates through history correctly\n- Use for modals, side panels, quick views\n- Masks can include different search params\n- Consider UX: users expect shared URLs to work\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4363,"content_sha256":"f31a9119bd67cbe6733e27e9e56072e32fd2da3cab19bc248a04aab773795fbc"},{"filename":"rules/org-virtual-routes.md","content":"# org-virtual-routes: Understand Virtual File Routes\n\n## Priority: LOW\n\n## Explanation\n\nVirtual routes are automatically generated placeholder routes in the route tree when you have a `.lazy.tsx` file without a corresponding main route file. They provide the minimal configuration needed to anchor lazy-loaded components.\n\n## Bad Example\n\n```tsx\n// Creating unnecessary boilerplate main route files\n// routes/settings.tsx - Just to have a file\nexport const Route = createFileRoute('/settings')({\n // Empty - no loader, no beforeLoad, nothing\n})\n\n// routes/settings.lazy.tsx - Actual component\nexport const Route = createLazyFileRoute('/settings')({\n component: SettingsPage,\n})\n\n// The main file is unnecessary boilerplate\n```\n\n## Good Example: Let Virtual Routes Handle It\n\n```tsx\n// Delete routes/settings.tsx entirely!\n\n// routes/settings.lazy.tsx - Only file needed\nexport const Route = createLazyFileRoute('/settings')({\n component: SettingsPage,\n pendingComponent: SettingsLoading,\n errorComponent: SettingsError,\n})\n\nfunction SettingsPage() {\n return \u003cdiv>Settings Content\u003c/div>\n}\n\n// TanStack Router auto-generates a virtual route:\n// {\n// path: '/settings',\n// // Minimal config to anchor the lazy file\n// }\n```\n\n## Good Example: When You DO Need Main Route File\n\n```tsx\n// routes/dashboard.tsx - Need this for loader/beforeLoad\nexport const Route = createFileRoute('/dashboard')({\n beforeLoad: async ({ context }) => {\n if (!context.auth.isAuthenticated) {\n throw redirect({ to: '/login' })\n }\n },\n loader: async ({ context: { queryClient } }) => {\n await queryClient.ensureQueryData(dashboardQueries.stats())\n },\n // Component is in lazy file\n})\n\n// routes/dashboard.lazy.tsx\nexport const Route = createLazyFileRoute('/dashboard')({\n component: DashboardPage,\n pendingComponent: DashboardSkeleton,\n})\n\n// Main file IS needed here because we have loader/beforeLoad\n```\n\n## Decision Guide\n\n| Route Has... | Need Main File? | Use Virtual? |\n|--------------|-----------------|--------------|\n| Only component | No | Yes |\n| loader | Yes | No |\n| beforeLoad | Yes | No |\n| validateSearch | Yes | No |\n| loaderDeps | Yes | No |\n| Just pendingComponent/errorComponent | No | Yes |\n\n## Good Example: File Structure with Virtual Routes\n\n```\nroutes/\n├── __root.tsx # Always needed\n├── index.tsx # Has loader\n├── about.lazy.tsx # Virtual route (no main file)\n├── contact.lazy.tsx # Virtual route (no main file)\n├── dashboard.tsx # Has beforeLoad (auth)\n├── dashboard.lazy.tsx # Component\n├── posts.tsx # Has loader\n├── posts.lazy.tsx # Component\n├── posts/\n│ ├── $postId.tsx # Has loader\n│ └── $postId.lazy.tsx # Component\n└── settings/\n ├── index.lazy.tsx # Virtual route\n ├── profile.lazy.tsx # Virtual route\n └── security.tsx # Has beforeLoad (requires re-auth)\n```\n\n## Good Example: Generated Route Tree\n\n```tsx\n// routeTree.gen.ts (auto-generated)\nimport { Route as rootRoute } from './routes/__root'\nimport { Route as aboutLazyRoute } from './routes/about.lazy' // Virtual parent\n\nexport const routeTree = rootRoute.addChildren([\n // Virtual route created for about.lazy.tsx\n createRoute({\n path: '/about',\n getParentRoute: () => rootRoute,\n }).lazy(() => import('./routes/about.lazy').then(m => m.Route)),\n\n // Regular route with explicit main file\n dashboardRoute.addChildren([...]),\n])\n```\n\n## Context\n\n- Virtual routes reduce boilerplate for simple pages\n- Only works with file-based routing\n- Auto-generated in `routeTree.gen.ts`\n- Main route file needed for any \"critical path\" config\n- Critical: loader, beforeLoad, validateSearch, loaderDeps, context\n- Non-critical (can be in lazy): component, pendingComponent, errorComponent\n- Check generated route tree to verify virtual routes\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3953,"content_sha256":"3cc24ad6357efec189872bc7c4cff267357da4ac888d83fe50a9bc013a1b8880"},{"filename":"rules/preload-intent.md","content":"# preload-intent: Enable Intent-Based Preloading\n\n## Priority: MEDIUM\n\n## Explanation\n\nConfigure `defaultPreload: 'intent'` to preload routes when users hover or focus links. This loads data before the click, making navigation feel instant.\n\n## Bad Example\n\n```tsx\n// No preloading configured - data loads after click\nconst router = createRouter({\n routeTree,\n // No defaultPreload - user waits after every navigation\n})\n\n// Each navigation shows loading state\nfunction PostList({ posts }: { posts: Post[] }) {\n return (\n \u003cul>\n {posts.map(post => (\n \u003cli key={post.id}>\n \u003cLink to=\"/posts/$postId\" params={{ postId: post.id }}>\n {post.title}\n \u003c/Link>\n {/* Click → wait for data → render */}\n \u003c/li>\n ))}\n \u003c/ul>\n )\n}\n```\n\n## Good Example\n\n```tsx\n// router.tsx - Enable preloading by default\nconst router = createRouter({\n routeTree,\n defaultPreload: 'intent', // Preload on hover/focus\n defaultPreloadDelay: 50, // Wait 50ms before starting\n})\n\ndeclare module '@tanstack/react-router' {\n interface Register {\n router: typeof router\n }\n}\n\n// Links automatically preload on hover\nfunction PostList({ posts }: { posts: Post[] }) {\n return (\n \u003cul>\n {posts.map(post => (\n \u003cli key={post.id}>\n \u003cLink to=\"/posts/$postId\" params={{ postId: post.id }}>\n {post.title}\n \u003c/Link>\n {/* Hover → preload starts → click → instant navigation */}\n \u003c/li>\n ))}\n \u003c/ul>\n )\n}\n```\n\n## Preload Options\n\n```tsx\n// Router-level defaults\nconst router = createRouter({\n routeTree,\n defaultPreload: 'intent', // 'intent' | 'render' | 'viewport' | false\n defaultPreloadDelay: 50, // ms before preload starts\n defaultPreloadStaleTime: 30000, // 30s - how long preloaded data stays fresh\n})\n\n// Link-level overrides\n\u003cLink\n to=\"/heavy-page\"\n preload={false} // Disable for this specific link\n>\n Heavy Page\n\u003c/Link>\n\n\u003cLink\n to=\"/critical-page\"\n preload=\"render\" // Preload immediately when Link renders\n>\n Critical Page\n\u003c/Link>\n```\n\n## Preload Strategies\n\n| Strategy | Behavior | Use Case |\n|----------|----------|----------|\n| `'intent'` | Preload on hover/focus | Default for most links |\n| `'render'` | Preload when Link mounts | Critical next pages |\n| `'viewport'` | Preload when Link enters viewport | Below-fold content |\n| `false` | No preloading | Heavy, rarely-visited pages |\n\n## Good Example: With TanStack Query Integration\n\n```tsx\n// When using TanStack Query, disable router cache\nconst router = createRouter({\n routeTree,\n defaultPreload: 'intent',\n defaultPreloadStaleTime: 0, // Let TanStack Query manage cache\n context: {\n queryClient,\n },\n})\n\n// Route loader uses TanStack Query\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params, context: { queryClient } }) => {\n // ensureQueryData respects TanStack Query's staleTime\n await queryClient.ensureQueryData(postQueries.detail(params.postId))\n },\n})\n```\n\n## Context\n\n- Preloading loads route code AND executes loaders\n- `preloadDelay` prevents excessive requests on quick mouse movements\n- Preloaded data is garbage collected after `preloadStaleTime`\n- Works with both router caching and external caching (TanStack Query)\n- Mobile: Consider `'viewport'` since hover isn't available\n- Monitor network tab to verify preloading works correctly\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3412,"content_sha256":"3b6b7b579dd8b519101a3d7b1461d6e6cf0d7e6b28cae87d5adf422212b05259"},{"filename":"rules/router-default-options.md","content":"# router-default-options: Configure Router Default Options\n\n## Priority: HIGH\n\n## Explanation\n\nTanStack Router's `createRouter` accepts several default options that apply globally. Configure these for consistent behavior across your application including error handling, scroll restoration, and performance optimizations.\n\n## Bad Example\n\n```tsx\n// Minimal router - missing useful defaults\nconst router = createRouter({\n routeTree,\n context: { queryClient },\n})\n\n// Each route must handle its own errors\n// No scroll restoration on navigation\n// No preloading configured\n```\n\n## Good Example: Full Configuration\n\n```tsx\nimport { QueryClient } from '@tanstack/react-query'\nimport { createRouter } from '@tanstack/react-router'\nimport { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'\nimport { routeTree } from './routeTree.gen'\nimport { DefaultCatchBoundary } from '@/components/DefaultCatchBoundary'\nimport { DefaultNotFound } from '@/components/DefaultNotFound'\n\nexport function getRouter() {\n const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n refetchOnWindowFocus: false,\n staleTime: 1000 * 60 * 2,\n },\n },\n })\n\n const router = createRouter({\n routeTree,\n context: { queryClient, user: null },\n\n // Preloading\n defaultPreload: 'intent', // Preload on hover/focus\n defaultPreloadStaleTime: 0, // Let Query manage freshness\n\n // Error handling\n defaultErrorComponent: DefaultCatchBoundary,\n defaultNotFoundComponent: DefaultNotFound,\n\n // UX\n scrollRestoration: true, // Restore scroll on back/forward\n\n // Performance\n defaultStructuralSharing: true, // Optimize re-renders\n })\n\n setupRouterSsrQueryIntegration({\n router,\n queryClient,\n })\n\n return router\n}\n```\n\n## Good Example: DefaultCatchBoundary Component\n\n```tsx\n// components/DefaultCatchBoundary.tsx\nimport { ErrorComponent, useRouter } from '@tanstack/react-router'\n\nexport function DefaultCatchBoundary({ error }: { error: Error }) {\n const router = useRouter()\n\n return (\n \u003cdiv className=\"error-container\">\n \u003ch1>Something went wrong\u003c/h1>\n \u003cErrorComponent error={error} />\n \u003cbutton onClick={() => router.invalidate()}>\n Try again\n \u003c/button>\n \u003c/div>\n )\n}\n```\n\n## Good Example: DefaultNotFound Component\n\n```tsx\n// components/DefaultNotFound.tsx\nimport { Link } from '@tanstack/react-router'\n\nexport function DefaultNotFound() {\n return (\n \u003cdiv className=\"not-found-container\">\n \u003ch1>404 - Page Not Found\u003c/h1>\n \u003cp>The page you're looking for doesn't exist.\u003c/p>\n \u003cLink to=\"/\">Go home\u003c/Link>\n \u003c/div>\n )\n}\n```\n\n## Router Options Reference\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `defaultPreload` | `false \\| 'intent' \\| 'render' \\| 'viewport'` | `false` | When to preload routes |\n| `defaultPreloadStaleTime` | `number` | `30000` | How long preloaded data stays fresh (ms) |\n| `defaultErrorComponent` | `Component` | Built-in | Global error boundary |\n| `defaultNotFoundComponent` | `Component` | Built-in | Global 404 page |\n| `scrollRestoration` | `boolean` | `false` | Restore scroll on navigation |\n| `defaultStructuralSharing` | `boolean` | `true` | Optimize loader data re-renders |\n\n## Good Example: Route-Level Overrides\n\n```tsx\n// Routes can override defaults\nexport const Route = createFileRoute('/admin')({\n // Custom error handling for admin section\n errorComponent: AdminErrorBoundary,\n notFoundComponent: AdminNotFound,\n\n // Disable preload for sensitive routes\n preload: false,\n})\n```\n\n## Good Example: With Pending Component\n\n```tsx\nconst router = createRouter({\n routeTree,\n context: { queryClient },\n\n defaultPreload: 'intent',\n defaultPreloadStaleTime: 0,\n defaultErrorComponent: DefaultCatchBoundary,\n defaultNotFoundComponent: DefaultNotFound,\n scrollRestoration: true,\n\n // Show during route transitions\n defaultPendingComponent: () => (\n \u003cdiv className=\"loading-bar\" />\n ),\n defaultPendingMinMs: 200, // Min time to show pending UI\n defaultPendingMs: 1000, // Delay before showing pending UI\n})\n```\n\n## Context\n\n- Set `defaultPreloadStaleTime: 0` when using TanStack Query\n- `scrollRestoration: true` improves back/forward navigation UX\n- `defaultStructuralSharing` prevents unnecessary re-renders\n- Route-level options override router defaults\n- Error/NotFound components receive route context\n- Pending components help with perceived performance\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4507,"content_sha256":"870e2976be4d71a5a32c29240ad4d6bf58fab97b149f4606174970d91f0fa0f6"},{"filename":"rules/search-custom-serializer.md","content":"# search-custom-serializer: Configure Custom Search Param Serializers\n\n## Priority: LOW\n\n## Explanation\n\nBy default, TanStack Router serializes search params as JSON. For cleaner URLs or compatibility with external systems, you can provide custom serializers using libraries like `qs`, `query-string`, or your own implementation.\n\n## Bad Example\n\n```tsx\n// Default JSON serialization creates ugly URLs\n// URL: /products?filters=%7B%22category%22%3A%22electronics%22%2C%22inStock%22%3Atrue%7D\n\n// Or manually parsing/serializing inconsistently\nfunction ProductList() {\n const searchParams = new URLSearchParams(window.location.search)\n const filters = JSON.parse(searchParams.get('filters') || '{}')\n // Inconsistent with router's handling\n}\n```\n\n## Good Example: Using JSURL for Compact URLs\n\n```tsx\nimport { createRouter } from '@tanstack/react-router'\nimport JSURL from 'jsurl2'\n\nconst router = createRouter({\n routeTree,\n search: {\n // Custom serializer for compact, URL-safe encoding\n serialize: (search) => JSURL.stringify(search),\n parse: (searchString) => JSURL.parse(searchString) || {},\n },\n})\n\n// URL: /products?~(category~'electronics~inStock~true)\n// Much shorter than JSON!\n```\n\n## Good Example: Using query-string for Flat Params\n\n```tsx\nimport { createRouter } from '@tanstack/react-router'\nimport queryString from 'query-string'\n\nconst router = createRouter({\n routeTree,\n search: {\n serialize: (search) =>\n queryString.stringify(search, {\n arrayFormat: 'bracket',\n skipNull: true,\n }),\n parse: (searchString) =>\n queryString.parse(searchString, {\n arrayFormat: 'bracket',\n parseBooleans: true,\n parseNumbers: true,\n }),\n },\n})\n\n// URL: /products?category=electronics&inStock=true&tags[]=sale&tags[]=new\n// Traditional query string format\n```\n\n## Good Example: Using qs for Nested Objects\n\n```tsx\nimport { createRouter } from '@tanstack/react-router'\nimport qs from 'qs'\n\nconst router = createRouter({\n routeTree,\n search: {\n serialize: (search) =>\n qs.stringify(search, {\n encodeValuesOnly: true,\n arrayFormat: 'brackets',\n }),\n parse: (searchString) =>\n qs.parse(searchString, {\n ignoreQueryPrefix: true,\n decoder(value) {\n // Parse booleans and numbers\n if (value === 'true') return true\n if (value === 'false') return false\n if (/^-?\\d+$/.test(value)) return parseInt(value, 10)\n return value\n },\n }),\n },\n})\n\n// URL: /products?filters[category]=electronics&filters[price][min]=100&filters[price][max]=500\n```\n\n## Good Example: Base64 for Complex State\n\n```tsx\nimport { createRouter } from '@tanstack/react-router'\n\nconst router = createRouter({\n routeTree,\n search: {\n serialize: (search) => {\n if (Object.keys(search).length === 0) return ''\n const json = JSON.stringify(search)\n return btoa(json) // Base64 encode\n },\n parse: (searchString) => {\n if (!searchString) return {}\n try {\n return JSON.parse(atob(searchString)) // Base64 decode\n } catch {\n return {}\n }\n },\n },\n})\n\n// URL: /products?eyJjYXRlZ29yeSI6ImVsZWN0cm9uaWNzIn0\n// Opaque but compact\n```\n\n## Good Example: Hybrid Approach\n\n```tsx\n// Some params as regular query, complex ones as JSON\nimport { createRouter } from '@tanstack/react-router'\n\nconst router = createRouter({\n routeTree,\n search: {\n serialize: (search) => {\n const { filters, ...simple } = search\n const params = new URLSearchParams()\n\n // Simple values as regular params\n Object.entries(simple).forEach(([key, value]) => {\n if (value !== undefined) {\n params.set(key, String(value))\n }\n })\n\n // Complex filters as JSON\n if (filters && Object.keys(filters).length > 0) {\n params.set('filters', JSON.stringify(filters))\n }\n\n return params.toString()\n },\n parse: (searchString) => {\n const params = new URLSearchParams(searchString)\n const result: Record\u003cstring, unknown> = {}\n\n params.forEach((value, key) => {\n if (key === 'filters') {\n result.filters = JSON.parse(value)\n } else if (value === 'true') {\n result[key] = true\n } else if (value === 'false') {\n result[key] = false\n } else if (/^-?\\d+$/.test(value)) {\n result[key] = parseInt(value, 10)\n } else {\n result[key] = value\n }\n })\n\n return result\n },\n },\n})\n\n// URL: /products?page=1&sort=price&filters={\"category\":\"electronics\",\"inStock\":true}\n```\n\n## Serializer Comparison\n\n| Library | URL Style | Best For |\n|---------|-----------|----------|\n| Default (JSON) | `?data=%7B...%7D` | TypeScript safety |\n| jsurl2 | `?~(key~'value)` | Compact, readable |\n| query-string | `?key=value&arr[]=1` | Traditional APIs |\n| qs | `?obj[nested]=value` | Deep nesting |\n| Base64 | `?eyJrZXkiOiJ2YWx1ZSJ9` | Opaque, compact |\n\n## Context\n\n- Custom serializers apply globally to all routes\n- Route-level `validateSearch` still works after parsing\n- Consider URL length limits (~2000 chars for safe cross-browser)\n- SEO: Search engines may not understand custom formats\n- Bookmarkability: Users can't easily modify opaque URLs\n- Debugging: JSON is easier to read in browser devtools\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5333,"content_sha256":"d8e6335e6ae8b33955f7e08cb2ff63767936897fec3f7f7db1e03aae217fda14"},{"filename":"rules/search-validation.md","content":"# search-validation: Always Validate Search Params\n\n## Priority: HIGH\n\n## Explanation\n\nSearch params come from the URL - user-controlled input that must be validated. Use `validateSearch` to parse, validate, and provide defaults. This ensures type safety and prevents runtime errors from malformed URLs.\n\n## Bad Example\n\n```tsx\n// No validation - trusting URL input directly\nexport const Route = createFileRoute('/products')({\n component: ProductsPage,\n})\n\nfunction ProductsPage() {\n // Accessing raw search params without validation\n const searchParams = new URLSearchParams(window.location.search)\n const page = parseInt(searchParams.get('page') || '1') // Could be NaN\n const sort = searchParams.get('sort') as 'asc' | 'desc' // Could be anything\n\n // Runtime errors possible if URL is malformed\n return \u003cProductList page={page} sort={sort} />\n}\n```\n\n## Good Example: Manual Validation\n\n```tsx\nexport const Route = createFileRoute('/products')({\n validateSearch: (search: Record\u003cstring, unknown>) => {\n return {\n page: Number(search.page) || 1,\n sort: search.sort === 'desc' ? 'desc' : 'asc',\n category: typeof search.category === 'string' ? search.category : undefined,\n minPrice: Number(search.minPrice) || undefined,\n maxPrice: Number(search.maxPrice) || undefined,\n }\n },\n component: ProductsPage,\n})\n\nfunction ProductsPage() {\n // Fully typed, validated search params\n const { page, sort, category, minPrice, maxPrice } = Route.useSearch()\n // page: number (default 1)\n // sort: 'asc' | 'desc' (default 'asc')\n // category: string | undefined\n}\n```\n\n## Good Example: With Zod\n\n```tsx\nimport { z } from 'zod'\n\nconst productSearchSchema = z.object({\n page: z.number().min(1).catch(1),\n limit: z.number().min(1).max(100).catch(20),\n sort: z.enum(['name', 'price', 'date']).catch('name'),\n order: z.enum(['asc', 'desc']).catch('asc'),\n category: z.string().optional(),\n search: z.string().optional(),\n minPrice: z.number().min(0).optional(),\n maxPrice: z.number().min(0).optional(),\n})\n\ntype ProductSearch = z.infer\u003ctypeof productSearchSchema>\n\nexport const Route = createFileRoute('/products')({\n validateSearch: (search) => productSearchSchema.parse(search),\n component: ProductsPage,\n})\n\nfunction ProductsPage() {\n const search = Route.useSearch()\n // search: ProductSearch - fully typed with defaults\n\n return (\n \u003cProductList\n page={search.page}\n limit={search.limit}\n sort={search.sort}\n order={search.order}\n filters={{\n category: search.category,\n search: search.search,\n priceRange: search.minPrice && search.maxPrice\n ? [search.minPrice, search.maxPrice]\n : undefined,\n }}\n />\n )\n}\n```\n\n## Good Example: With Valibot\n\n```tsx\nimport * as v from 'valibot'\nimport { valibotSearchValidator } from '@tanstack/router-valibot-adapter'\n\nconst searchSchema = v.object({\n page: v.fallback(v.number(), 1),\n query: v.fallback(v.string(), ''),\n filters: v.fallback(\n v.array(v.string()),\n []\n ),\n})\n\nexport const Route = createFileRoute('/search')({\n validateSearch: valibotSearchValidator(searchSchema),\n component: SearchPage,\n})\n```\n\n## Updating Search Params\n\n```tsx\nfunction ProductFilters() {\n const navigate = useNavigate()\n const search = Route.useSearch()\n\n const updateFilters = (newFilters: Partial\u003cProductSearch>) => {\n navigate({\n to: '.', // Current route\n search: (prev) => ({\n ...prev,\n ...newFilters,\n page: 1, // Reset to page 1 when filters change\n }),\n })\n }\n\n return (\n \u003cdiv>\n \u003cselect\n value={search.sort}\n onChange={(e) => updateFilters({ sort: e.target.value as ProductSearch['sort'] })}\n >\n \u003coption value=\"name\">Name\u003c/option>\n \u003coption value=\"price\">Price\u003c/option>\n \u003coption value=\"date\">Date\u003c/option>\n \u003c/select>\n \u003c/div>\n )\n}\n```\n\n## Context\n\n- Search params are user input - never trust them unvalidated\n- Use `.catch()` in Zod or `fallback()` in Valibot for graceful defaults\n- Validation runs on every navigation - keep it fast\n- Search params are inherited by child routes\n- Use `search` updater function to preserve other params\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4211,"content_sha256":"e83d656bc9c60c7ec088ad8d0044c77f5b602384973ad2573ae2022b0c7e20a9"},{"filename":"rules/split-lazy-routes.md","content":"# split-lazy-routes: Use .lazy.tsx for Code Splitting\n\n## Priority: MEDIUM\n\n## Explanation\n\nSplit route components into `.lazy.tsx` files to reduce initial bundle size. The main route file keeps critical configuration (path, loaders, search validation), while lazy files contain components that load on-demand.\n\n## Bad Example\n\n```tsx\n// routes/dashboard.tsx - Everything in one file\nimport { createFileRoute } from '@tanstack/react-router'\nimport { HeavyChartLibrary } from 'heavy-chart-library'\nimport { ComplexDataGrid } from 'complex-data-grid'\nimport { AnalyticsWidgets } from './components/AnalyticsWidgets'\n\nexport const Route = createFileRoute('/dashboard')({\n loader: async ({ context }) => {\n return context.queryClient.ensureQueryData(dashboardQueries.stats())\n },\n component: DashboardPage, // Entire component in main bundle\n})\n\nfunction DashboardPage() {\n // Heavy components loaded even if user never visits dashboard\n return (\n \u003cdiv>\n \u003cHeavyChartLibrary data={useLoaderData()} />\n \u003cComplexDataGrid />\n \u003cAnalyticsWidgets />\n \u003c/div>\n )\n}\n```\n\n## Good Example\n\n```tsx\n// routes/dashboard.tsx - Only critical config\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/dashboard')({\n loader: async ({ context }) => {\n return context.queryClient.ensureQueryData(dashboardQueries.stats())\n },\n // No component - it's in the lazy file\n})\n\n// routes/dashboard.lazy.tsx - Lazy-loaded component\nimport { createLazyFileRoute } from '@tanstack/react-router'\nimport { HeavyChartLibrary } from 'heavy-chart-library'\nimport { ComplexDataGrid } from 'complex-data-grid'\nimport { AnalyticsWidgets } from './components/AnalyticsWidgets'\n\nexport const Route = createLazyFileRoute('/dashboard')({\n component: DashboardPage,\n pendingComponent: DashboardSkeleton,\n errorComponent: DashboardError,\n})\n\nfunction DashboardPage() {\n const data = Route.useLoaderData()\n return (\n \u003cdiv>\n \u003cHeavyChartLibrary data={data} />\n \u003cComplexDataGrid />\n \u003cAnalyticsWidgets />\n \u003c/div>\n )\n}\n\nfunction DashboardSkeleton() {\n return \u003cdiv className=\"dashboard-skeleton\">Loading dashboard...\u003c/div>\n}\n\nfunction DashboardError({ error }: { error: Error }) {\n return \u003cdiv>Failed to load dashboard: {error.message}\u003c/div>\n}\n```\n\n## What Goes Where\n\n```tsx\n// Main route file (routes/example.tsx)\n// - path configuration (implicit from file location)\n// - validateSearch\n// - beforeLoad (auth checks, redirects)\n// - loader (data fetching)\n// - loaderDeps\n// - context manipulation\n// - Static route data\n\n// Lazy file (routes/example.lazy.tsx)\n// - component\n// - pendingComponent\n// - errorComponent\n// - notFoundComponent\n```\n\n## Using getRouteApi in Lazy Components\n\n```tsx\n// routes/posts/$postId.lazy.tsx\nimport { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'\n\nconst route = getRouteApi('/posts/$postId')\n\nexport const Route = createLazyFileRoute('/posts/$postId')({\n component: PostPage,\n})\n\nfunction PostPage() {\n // Type-safe access without importing main route file\n const { postId } = route.useParams()\n const data = route.useLoaderData()\n\n return \u003carticle>{/* ... */}\u003c/article>\n}\n```\n\n## Automatic Code Splitting\n\n```tsx\n// vite.config.ts - Enable automatic splitting\nimport { TanStackRouterVite } from '@tanstack/router-plugin/vite'\n\nexport default defineConfig({\n plugins: [\n TanStackRouterVite({\n autoCodeSplitting: true, // Automatically splits all route components\n }),\n react(),\n ],\n})\n\n// With autoCodeSplitting, you don't need .lazy.tsx files\n// The plugin handles the splitting automatically\n```\n\n## Context\n\n- Lazy loading reduces initial bundle size significantly\n- Loaders are NOT lazy - they need to run before rendering\n- `createLazyFileRoute` only accepts component-related options\n- Use `getRouteApi()` for type-safe hook access in lazy files\n- Consider `autoCodeSplitting: true` for simpler setup\n- Virtual routes auto-generate when only .lazy.tsx exists\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4000,"content_sha256":"9a46b3f07de29b4f9c5424be1a2483549bc3c14bef299cd6f42f3b66549ee399"},{"filename":"rules/ts-register-router.md","content":"# ts-register-router: Register Router Type for Global Inference\n\n## Priority: CRITICAL\n\n## Explanation\n\nRegister your router instance with TypeScript's module declaration to enable type inference across your entire application. Without registration, hooks like `useNavigate`, `useParams`, and `useSearch` won't know your route structure.\n\n## Bad Example\n\n```tsx\n// router.tsx - Missing type registration\nimport { createRouter, createRootRoute } from '@tanstack/react-router'\nimport { routeTree } from './routeTree.gen'\n\nexport const router = createRouter({ routeTree })\n\n// components/Navigation.tsx\nimport { useNavigate } from '@tanstack/react-router'\n\nfunction Navigation() {\n const navigate = useNavigate()\n\n // TypeScript doesn't know valid routes - no autocomplete or type checking\n navigate({ to: '/posts/$postId' }) // No error even if route doesn't exist\n}\n```\n\n## Good Example\n\n```tsx\n// router.tsx\nimport { createRouter } from '@tanstack/react-router'\nimport { routeTree } from './routeTree.gen'\n\nexport const router = createRouter({ routeTree })\n\n// Register the router instance for type inference\ndeclare module '@tanstack/react-router' {\n interface Register {\n router: typeof router\n }\n}\n\n// components/Navigation.tsx\nimport { useNavigate } from '@tanstack/react-router'\n\nfunction Navigation() {\n const navigate = useNavigate()\n\n // Full type safety - TypeScript knows all valid routes\n navigate({ to: '/posts/$postId', params: { postId: '123' } })\n\n // Type error if route doesn't exist\n navigate({ to: '/invalid-route' }) // Error: Type '\"/invalid-route\"' is not assignable...\n\n // Autocomplete for params\n navigate({\n to: '/users/$userId/posts/$postId',\n params: { userId: '1', postId: '2' }, // Both required\n })\n}\n```\n\n## Benefits of Registration\n\n```tsx\n// After registration, all these get full type inference:\n\n// 1. Navigation\nconst navigate = useNavigate()\nnavigate({ to: '/posts/$postId', params: { postId: '123' } })\n\n// 2. Link component\n\u003cLink to=\"/posts/$postId\" params={{ postId: '123' }}>View Post\u003c/Link>\n\n// 3. useParams hook\nconst { postId } = useParams({ from: '/posts/$postId' }) // postId: string\n\n// 4. useSearch hook\nconst search = useSearch({ from: '/posts' }) // Knows search param types\n\n// 5. useLoaderData hook\nconst data = useLoaderData({ from: '/posts/$postId' }) // Knows loader return type\n```\n\n## File-Based Routing Setup\n\n```tsx\n// With file-based routing, routeTree is auto-generated\n// router.tsx\nimport { createRouter } from '@tanstack/react-router'\nimport { routeTree } from './routeTree.gen' // Generated file\n\nexport const router = createRouter({\n routeTree,\n defaultPreload: 'intent',\n})\n\ndeclare module '@tanstack/react-router' {\n interface Register {\n router: typeof router\n }\n}\n```\n\n## Context\n\n- Must be done once, typically in your router configuration file\n- Enables IDE autocomplete for routes, params, and search params\n- Catches invalid routes at compile time\n- Works with both file-based and code-based routing\n- Required for full TypeScript benefits of TanStack Router\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3070,"content_sha256":"5ecba212565a6e9125378aa1d1ee04fcd7ef081050eb00787c674565353eea25"},{"filename":"rules/ts-use-from-param.md","content":"# ts-use-from-param: Use `from` Parameter for Type Narrowing\n\n## Priority: CRITICAL\n\n## Explanation\n\nWhen using hooks like `useParams`, `useSearch`, or `useLoaderData`, provide the `from` parameter to get exact types for that route. Without it, TypeScript returns a union of all possible types across all routes.\n\n## Bad Example\n\n```tsx\n// Without 'from' - TypeScript doesn't know which route's types to use\nfunction PostDetail() {\n // params could be from ANY route - types are unioned\n const params = useParams()\n // params: { postId?: string; userId?: string; categoryId?: string; ... }\n\n // TypeScript can't guarantee postId exists\n console.log(params.postId) // postId: string | undefined\n}\n\n// Similarly for search params\nfunction SearchResults() {\n const search = useSearch()\n // search: union of ALL routes' search params\n}\n```\n\n## Good Example\n\n```tsx\n// With 'from' - exact types for this specific route\nfunction PostDetail() {\n const params = useParams({ from: '/posts/$postId' })\n // params: { postId: string } - exactly what this route provides\n\n console.log(params.postId) // postId: string (guaranteed)\n}\n\n// Full path matching\nfunction UserPost() {\n const params = useParams({ from: '/users/$userId/posts/$postId' })\n // params: { userId: string; postId: string }\n}\n\n// Search params with type narrowing\nfunction SearchResults() {\n const search = useSearch({ from: '/search' })\n // search: exactly the validated search params for /search route\n}\n\n// Loader data with type inference\nfunction PostPage() {\n const { post, comments } = useLoaderData({ from: '/posts/$postId' })\n // Exact types from your loader function\n}\n```\n\n## Using Route.fullPath for Type Safety\n\n```tsx\n// routes/posts/$postId.tsx\nimport { createFileRoute } from '@tanstack/react-router'\n\nexport const Route = createFileRoute('/posts/$postId')({\n loader: async ({ params }) => {\n const post = await fetchPost(params.postId)\n return { post }\n },\n component: PostComponent,\n})\n\nfunction PostComponent() {\n // Use Route.fullPath for guaranteed type matching\n const params = useParams({ from: Route.fullPath })\n const { post } = useLoaderData({ from: Route.fullPath })\n\n // Or use route-specific helper (preferred in same file)\n const { postId } = Route.useParams()\n const data = Route.useLoaderData()\n}\n```\n\n## Using getRouteApi for Code-Split Components\n\n```tsx\n// components/PostDetail.tsx (separate file from route)\nimport { getRouteApi } from '@tanstack/react-router'\n\n// Get type-safe access without importing the route\nconst postRoute = getRouteApi('/posts/$postId')\n\nexport function PostDetail() {\n const params = postRoute.useParams()\n // params: { postId: string }\n\n const data = postRoute.useLoaderData()\n // data: exact loader return type\n\n const search = postRoute.useSearch()\n // search: exact search param types\n}\n```\n\n## When to Use strict: false\n\n```tsx\n// In shared components that work across multiple routes\nfunction Breadcrumbs() {\n // strict: false returns union types but allows component reuse\n const params = useParams({ strict: false })\n const location = useLocation()\n\n // params may or may not have certain values\n return (\n \u003cnav>\n {params.userId && \u003cspan>User: {params.userId}\u003c/span>}\n {params.postId && \u003cspan>Post: {params.postId}\u003c/span>}\n \u003c/nav>\n )\n}\n```\n\n## Context\n\n- Always use `from` in route-specific components for exact types\n- Use `Route.useParams()` / `Route.useLoaderData()` within route files\n- Use `getRouteApi()` in components split from route files\n- Use `strict: false` only in truly generic, cross-route components\n- The `from` path must match exactly (including params like `$postId`)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3674,"content_sha256":"ff9ef42c135d2909f9a23c9659d86c0b0fb4d27d298fc8795af96a959ed05077"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"TanStack Router Best Practices","type":"text"}]},{"type":"paragraph","content":[{"text":"Comprehensive guidelines for implementing TanStack Router patterns in React applications. These rules optimize type safety, data loading, navigation, and code organization.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Apply","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up application routing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creating new routes and layouts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementing search parameter handling","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Configuring data loaders","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up code splitting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integrating with TanStack Query","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Refactoring navigation patterns","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rule Categories by Priority","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Priority","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rules","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type Safety","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prevents runtime errors and enables refactoring","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Route Organization","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ensures maintainable route structure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Router Config","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1 rule","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Global router defaults","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Data Loading","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Optimizes data fetching and caching","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Search Params","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enables type-safe URL state","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error Handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1 rule","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Handles 404 and errors gracefully","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Navigation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Improves UX and accessibility","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Code Splitting","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reduces bundle size","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Preloading","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Improves perceived performance","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LOW","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Route Context","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3 rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Enables dependency injection","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Type Safety (Prefix: ","type":"text"},{"text":"ts-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ts-register-router","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Register router type for global inference","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ts-use-from-param","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use ","type":"text"},{"text":"from","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter for type narrowing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ts-route-context-typing","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Type route context with createRootRouteWithContext","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ts-query-options-loader","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use queryOptions in loaders for type inference","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Router Config (Prefix: ","type":"text"},{"text":"router-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"router-default-options","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Configure router defaults (scrollRestoration, defaultErrorComponent, etc.)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Route Organization (Prefix: ","type":"text"},{"text":"org-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"org-file-based-routing","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Prefer file-based routing for conventions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"org-route-tree-structure","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Follow hierarchical route tree patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"org-pathless-layouts","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use pathless routes for shared layouts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"org-index-routes","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Understand index vs layout routes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"org-virtual-routes","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Understand virtual file routes","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data Loading (Prefix: ","type":"text"},{"text":"load-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"load-use-loaders","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use route loaders for data fetching","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"load-loader-deps","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Define loaderDeps for cache control","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"load-ensure-query-data","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use ensureQueryData with TanStack Query","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"load-deferred-data","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Split critical and non-critical data","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"load-error-handling","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Handle loader errors appropriately","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"load-parallel","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Leverage parallel route loading","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Search Params (Prefix: ","type":"text"},{"text":"search-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search-validation","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Always validate search params","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search-type-inheritance","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Leverage parent search param types","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search-middleware","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use search param middleware","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search-defaults","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Provide sensible defaults","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"search-custom-serializer","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Configure custom search param serializers","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Error Handling (Prefix: ","type":"text"},{"text":"err-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"err-not-found","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Handle not-found routes properly","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Navigation (Prefix: ","type":"text"},{"text":"nav-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"nav-link-component","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Prefer Link component for navigation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"nav-active-states","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Configure active link states","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"nav-use-navigate","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use useNavigate for programmatic navigation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"nav-relative-paths","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Understand relative path navigation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"nav-route-masks","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use route masks for modal URLs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Code Splitting (Prefix: ","type":"text"},{"text":"split-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"split-lazy-routes","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use .lazy.tsx for code splitting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"split-critical-path","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Keep critical config in main route file","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"split-auto-splitting","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Enable autoCodeSplitting when possible","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Preloading (Prefix: ","type":"text"},{"text":"preload-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"preload-intent","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Enable intent-based preloading","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"preload-stale-time","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Configure preload stale time","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"preload-manual","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use manual preloading strategically","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Route Context (Prefix: ","type":"text"},{"text":"ctx-","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ctx-root-context","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Define context at root route","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ctx-before-load","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Extend context in beforeLoad","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ctx-dependency-injection","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Use context for dependency injection","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Each rule file in the ","type":"text"},{"text":"rules/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory contains:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Explanation","type":"text","marks":[{"type":"strong"}]},{"text":" — Why this pattern matters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Bad Example","type":"text","marks":[{"type":"strong"}]},{"text":" — Anti-pattern to avoid","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Good Example","type":"text","marks":[{"type":"strong"}]},{"text":" — Recommended implementation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Context","type":"text","marks":[{"type":"strong"}]},{"text":" — When to apply or skip this rule","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Full Reference","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"See individual rule files in ","type":"text"},{"text":"rules/","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory for detailed guidance and code examples.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"tanstack-router-best-practices","author":"@skillopedia","source":{"stars":178,"repo_name":"tanstack-agent-skills","origin_url":"https://github.com/deckardger/tanstack-agent-skills/blob/HEAD/skills/tanstack-router/SKILL.md","repo_owner":"deckardger","body_sha256":"67f12bf2c57c052263e8db80c3d4d89c16eb8f4e1ddc72b8b5d211b29e4212ca","cluster_key":"92cf8ab8dc55476fc5132c8e3540d3442b5d10f1e3f28a10150fdded771a8348","clean_bundle":{"format":"clean-skill-bundle-v1","source":"deckardger/tanstack-agent-skills/skills/tanstack-router/SKILL.md","attachments":[{"id":"4da01190-5593-5975-a53a-a8e9112c5f2a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4da01190-5593-5975-a53a-a8e9112c5f2a/attachment.md","path":"rules/ctx-root-context.md","size":4521,"sha256":"678ea466820f4161e43c3515167b845e21487a1b7b748a9e59ea7c23f8b40672","contentType":"text/markdown; charset=utf-8"},{"id":"e94e54cc-1729-53db-9876-4f593220342e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e94e54cc-1729-53db-9876-4f593220342e/attachment.md","path":"rules/err-not-found.md","size":4679,"sha256":"44628dbd8baa8e25fde9fe3a55f478d9cac9afb7a4bdd5c1e834a85a658daa60","contentType":"text/markdown; charset=utf-8"},{"id":"560e2cf0-6399-5b68-9e2b-11e3616c8ddd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/560e2cf0-6399-5b68-9e2b-11e3616c8ddd/attachment.md","path":"rules/load-ensure-query-data.md","size":4211,"sha256":"6cb40748f915a64058dbcaf33aadf080f7a2b9324b2c6e4c1e127dcb71259edf","contentType":"text/markdown; charset=utf-8"},{"id":"18257037-5806-561d-baa4-fd828e0c3163","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/18257037-5806-561d-baa4-fd828e0c3163/attachment.md","path":"rules/load-parallel.md","size":5408,"sha256":"9f9357380d0df5a1a3c3da1b8c5f15ad056842a436467be3ab33e097612f0185","contentType":"text/markdown; charset=utf-8"},{"id":"f8da65b0-7029-5794-80cd-c199cf9c32d3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f8da65b0-7029-5794-80cd-c199cf9c32d3/attachment.md","path":"rules/load-use-loaders.md","size":3883,"sha256":"2804995a36ab6fbdee628ce73c9c72148b8c010117620adcbfeb9bb163366fd7","contentType":"text/markdown; charset=utf-8"},{"id":"9cd1bebd-20a4-5eb0-b49e-f14c9d11c9a6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9cd1bebd-20a4-5eb0-b49e-f14c9d11c9a6/attachment.md","path":"rules/nav-link-component.md","size":4065,"sha256":"a8ed634f01d280e46c8552020e4abab1fa2ad53655329bb71648cfa956f287d1","contentType":"text/markdown; charset=utf-8"},{"id":"9a3eca3f-70cd-5a20-9adb-0c160b452663","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9a3eca3f-70cd-5a20-9adb-0c160b452663/attachment.md","path":"rules/nav-route-masks.md","size":4363,"sha256":"f31a9119bd67cbe6733e27e9e56072e32fd2da3cab19bc248a04aab773795fbc","contentType":"text/markdown; charset=utf-8"},{"id":"f3544c5a-f180-5989-820f-d69657a69925","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f3544c5a-f180-5989-820f-d69657a69925/attachment.md","path":"rules/org-virtual-routes.md","size":3953,"sha256":"3cc24ad6357efec189872bc7c4cff267357da4ac888d83fe50a9bc013a1b8880","contentType":"text/markdown; charset=utf-8"},{"id":"4c53a5ae-18a3-51ab-8614-66382f473e3e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4c53a5ae-18a3-51ab-8614-66382f473e3e/attachment.md","path":"rules/preload-intent.md","size":3412,"sha256":"3b6b7b579dd8b519101a3d7b1461d6e6cf0d7e6b28cae87d5adf422212b05259","contentType":"text/markdown; charset=utf-8"},{"id":"7244d8f3-a64f-5513-acbd-217e1ea9b778","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7244d8f3-a64f-5513-acbd-217e1ea9b778/attachment.md","path":"rules/router-default-options.md","size":4507,"sha256":"870e2976be4d71a5a32c29240ad4d6bf58fab97b149f4606174970d91f0fa0f6","contentType":"text/markdown; charset=utf-8"},{"id":"f79a028d-3dd1-557b-a7a6-e736786f6b5d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f79a028d-3dd1-557b-a7a6-e736786f6b5d/attachment.md","path":"rules/search-custom-serializer.md","size":5333,"sha256":"d8e6335e6ae8b33955f7e08cb2ff63767936897fec3f7f7db1e03aae217fda14","contentType":"text/markdown; charset=utf-8"},{"id":"d3e8a348-d4f0-5ab6-b62b-825a446f7bc5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3e8a348-d4f0-5ab6-b62b-825a446f7bc5/attachment.md","path":"rules/search-validation.md","size":4211,"sha256":"e83d656bc9c60c7ec088ad8d0044c77f5b602384973ad2573ae2022b0c7e20a9","contentType":"text/markdown; charset=utf-8"},{"id":"0bfe5306-d84a-5416-bd49-3d493dc364ba","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0bfe5306-d84a-5416-bd49-3d493dc364ba/attachment.md","path":"rules/split-lazy-routes.md","size":4000,"sha256":"9a46b3f07de29b4f9c5424be1a2483549bc3c14bef299cd6f42f3b66549ee399","contentType":"text/markdown; charset=utf-8"},{"id":"d7107d63-6c4d-5625-b045-84361fe162e2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d7107d63-6c4d-5625-b045-84361fe162e2/attachment.md","path":"rules/ts-register-router.md","size":3070,"sha256":"5ecba212565a6e9125378aa1d1ee04fcd7ef081050eb00787c674565353eea25","contentType":"text/markdown; charset=utf-8"},{"id":"843439e1-d667-55db-a92a-0f62d086cc35","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/843439e1-d667-55db-a92a-0f62d086cc35/attachment.md","path":"rules/ts-use-from-param.md","size":3674,"sha256":"ff9ef42c135d2909f9a23c9659d86c0b0fb4d27d298fc8795af96a959ed05077","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"568b55d69e807413fbccbbdd24b54b894237a9dceb23f59d8e924ea010064b62","attachment_count":15,"text_attachments":15,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/tanstack-router/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"TanStack Router best practices for type-safe routing, data loading, search params, and navigation. Activate when building React applications with complex routing needs."}},"renderedAt":1782987525683}

TanStack Router Best Practices Comprehensive guidelines for implementing TanStack Router patterns in React applications. These rules optimize type safety, data loading, navigation, and code organization. When to Apply - Setting up application routing - Creating new routes and layouts - Implementing search parameter handling - Configuring data loaders - Setting up code splitting - Integrating with TanStack Query - Refactoring navigation patterns Rule Categories by Priority | Priority | Category | Rules | Impact | |----------|----------|-------|--------| | CRITICAL | Type Safety | 4 rules | Pre…