mcpField
T
tanstack-router

tanstack-router

github.com/blencorp开发11190 次安装49 周前更新
代码仓库

关于 tanstack-router

面向 TanStack Router 项目的路由模式指南,适用于实现文件路由、类型安全导航和路由数据加载。还介绍懒加载、搜索参数及路由守卫。

主要功能

  1. 01文件式路由与路径参数
  2. 02路由加载器获取数据
  3. 03类型安全导航与搜索参数
  4. 04组件和加载器懒加载
  5. 05认证与权限路由守卫

适用场景

  1. 01按文件结构创建路由
  2. 02加载带参数的路由数据
  3. 03实现筛选参数和程序化导航
  4. 04限制管理页面访问

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/blencorp/claude-code-kit --skill tanstack-router

安装后在 Claude Code 中输入/tanstack-router确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

skill-rules-fragment.json
resources/
navigation-patterns.md
route-loaders.md
routing-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>

其他资源

更多模式请参阅:

评论

支持 Markdown