关于 tanstack-router
面向 TanStack Router 项目的路由模式指南,适用于实现文件路由、类型安全导航和路由数据加载。还介绍懒加载、搜索参数及路由守卫。
主要功能
- 01文件式路由与路径参数
- 02路由加载器获取数据
- 03类型安全导航与搜索参数
- 04组件和加载器懒加载
- 05认证与权限路由守卫
适用场景
- 01按文件结构创建路由
- 02加载带参数的路由数据
- 03实现筛选参数和程序化导航
- 04限制管理页面访问
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ blencorp/ claude-code-kit --skill tanstack-router安装后在 Claude Code 中输入/tanstack-router确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
skill-rules-fragment.jsonresources/navigation-patterns.mdroute-loaders.mdrouting-guide.md详细介绍
TanStack Router 模式
用途
使用 TanStack Router 进行基于文件的路由,重点关注类型安全的导航、路由加载器和懒加载。
何时使用此技能
- 创建新路由
- 实现导航
- 使用路由加载器获取数据
- 使用参数进行类型安全的路由
- 懒加载路由
快速入门
基本路由
// 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,
});
最佳实践
1. 使用加载器获取数据
// ✅ 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>;
}
2. 懒加载大型路由
// ✅ Good: Lazy load admin panel
const AdminPanel = lazy(() => import('~/features/admin/AdminPanel'));
export const Route = createFileRoute('/admin')({
component: AdminPanel,
});
3. 类型安全的导航
// ✅ 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>
其他资源
更多模式请参阅:
- routing-guide.md - 高级路由
- navigation-patterns.md - 导航策略
- route-loaders.md - 复杂加载器
评论
支持 Markdown