TanStack Router file-based routing patterns including route creation, navigation, loaders, type-safe routing, and lazy loading. Use when creating routes, implementing navigation, or working with TanStack Router.
File-based routing with TanStack Router, emphasizing type-safe navigation, route loaders, and lazy loading.
// routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { postsApi } from '~/features/posts/api/postsApi';
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await postsApi.getAll();
return { posts };
},
component: PostsPage,
});
function PostsPage() {
const { posts } = Route.useLoaderData();
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
routes/
├── __root.tsx # Root route
├── index.tsx # /
├── about.tsx # /about
├── posts/
│ ├── index.tsx # /posts
│ └── $postId.tsx # /posts/:postId
└── users/
├── index.tsx # /users
└── $userId/
├── index.tsx # /users/:userId
└── posts.tsx # /users/:userId/posts
File Path → URL Path
routes/index.tsx → /
routes/about.tsx → /about
routes/posts/index.tsx → /posts
routes/posts/$postId.tsx → /posts/:postId
routes/users/$userId/index.tsx → /users/:userId
routes/users/$userId/posts.tsx → /users/:userId/posts
// routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router';
import { postsApi } from '~/features/posts/api/postsApi';
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await postsApi.get(params.postId);
return { post };
},
component: PostDetails,
});
function PostDetails() {
const { post } = Route.useLoaderData();
const { postId } = Route.useParams();
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
// routes/users/$userId/posts/$postId.tsx
export const Route = createFileRoute('/users/$userId/posts/$postId')({
loader: async ({ params }) => {
const { userId, postId } = params;
const post = await postsApi.getByUserAndId(userId, postId);
return { post };
},
component: UserPostDetails,
});
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await postsApi.getAll();
return { posts };
},
component: PostsPage,
});
export const Route = createFileRoute('/users/$userId/posts')({
loader: async ({ params, context }) => {
const [user, posts] = await Promise.all([
userApi.get(params.userId),
postsApi.getByUser(params.userId),
]);
return { user, posts };
},
component: UserPosts,
});
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
try {
const post = await postsApi.get(params.postId);
return { post, error: null };
} catch (error) {
return { post: null, error: 'Post not found' };
}
},
component: PostDetails,
});
function PostDetails() {
const { post, error } = Route.useLoaderData();
if (error) return <Error message={error} />;
return <div>{post.title}</div>;
}
import { Link, useNavigate } from '@tanstack/react-router';
// Link component
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link>
// Programmatic navigation
const navigate = useNavigate();
navigate({ to: '/posts', search: { filter: 'published' } });
// routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { lazy } from 'react';
const PostsPage = lazy(() => import('~/features/posts/PostsPage'));
export const Route = createFileRoute('/posts')({
component: PostsPage,
});
export const Route = createFileRoute('/posts')({
loader: async () => {
// Dynamically import heavy module only when route loads
const { processData } = await import('~/lib/heavyModule');
const posts = await postsApi.getAll();
const processed = processData(posts);
return { posts: processed };
},
component: PostsPage,
});
import { z } from 'zod';
const postsSearchSchema = z.object({
filter: z.enum(['all', 'published', 'draft']).default('all'),
sort: z.enum(['date', 'title']).default('date'),
page: z.number().default(1),
});
export const Route = createFileRoute('/posts')({
validateSearch: postsSearchSchema,
loader: async ({ search }) => {
const posts = await postsApi.getAll(search);
return { posts };
},
component: PostsPage,
});
function PostsPage() {
const { posts } = Route.useLoaderData();
const search = Route.useSearch();
return (
<div>
<p>Filter: {search.filter}</p>
<p>Sort: {search.sort}</p>
<p>Page: {search.page}</p>
</div>
);
}
import { useNavigate } from '@tanstack/react-router';
function FilterButtons() {
const navigate = useNavigate();
const search = Route.useSearch();
const setFilter = (filter: string) => {
navigate({
to: '.',
search: (prev) => ({ ...prev, filter }),
});
};
return (
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('published')}>Published</button>
<button onClick={() => setFilter('draft')}>Draft</button>
</div>
);
}
// routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';
export const Route = createRootRoute({
component: RootLayout,
});
function RootLayout() {
return (
<div>
<Header />
<main>
<Outlet /> {/* Child routes render here */}
</main>
<Footer />
</div>
);
}
// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
component: DashboardLayout,
});
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<div className="content">
<Outlet /> {/* Dashboard child routes */}
</div>
</div>
);
}
// routes/dashboard/index.tsx
export const Route = createFileRoute('/dashboard')({
component: DashboardHome,
});
// routes/dashboard/analytics.tsx
export const Route = createFileRoute('/dashboard/analytics')({
component: Analytics,
});
export const Route = createFileRoute('/admin')({
beforeLoad: async ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: '/admin' },
});
}
},
component: AdminPage,
});
export const Route = createFileRoute('/admin/users')({
beforeLoad: async ({ context }) => {
if (!context.auth.hasPermission('users:manage')) {
throw redirect({ to: '/unauthorized' });
}
},
component: UsersPage,
});
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await postsApi.get(params.postId);
return { post };
},
meta: ({ loaderData }) => [
{ title: 'Home', path: '/' },
{ title: 'Posts', path: '/posts' },
{ title: loaderData.post.title, path: `/posts/${loaderData.post.id}` },
],
component: PostDetails,
});
// ✅ Good: Loader fetches data
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await postsApi.getAll();
return { posts };
},
component: PostsPage,
});
// ❌ Avoid: Fetching in component
function PostsPage() {
const [posts, setPosts] = useState([]);
useEffect(() => {
postsApi.getAll().then(setPosts);
}, []);
return <div>...</div>;
}
// ✅ Good: Lazy load admin panel
const AdminPanel = lazy(() => import('~/features/admin/AdminPanel'));
export const Route = createFileRoute('/admin')({
component: AdminPanel,
});
// ✅ Good: Type-safe Link
<Link to="/posts/$postId" params={{ postId: post.id }}>
View Post
</Link>
// ❌ Avoid: String concatenation
<a href={`/posts/${post.id}`}>View Post</a>
For more patterns, see:
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer