tanstack-router

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Router Patterns

TanStack Router 路由模式

Purpose

用途

File-based routing with TanStack Router, emphasizing type-safe navigation, route loaders, and lazy loading.
使用TanStack Router实现基于文件的路由,重点介绍类型安全导航、路由加载器和懒加载。

When to Use This Skill

何时使用该技能

  • Creating new routes
  • Implementing navigation
  • Using route loaders for data
  • Type-safe routing with parameters
  • Lazy loading routes

  • 创建新路由
  • 实现导航功能
  • 使用路由加载器获取数据
  • 带参数的类型安全路由
  • 懒加载路由

Quick Start

快速开始

Basic Route

基础路由

typescript
// 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>
  );
}

typescript
// 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>文章列表</h1>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

File-Based Routing

基于文件的路由

Directory Structure

目录结构

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
routes/
├── __root.tsx          # 根路由
├── 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

Route Mapping

路由映射

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

文件路径                        → URL 路径
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

Route Parameters

路由参数

Dynamic Routes

动态路由

typescript
// 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>
  );
}
typescript
// 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>
  );
}

Multiple Parameters

多参数路由

typescript
// 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,
});

typescript
// 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,
});

Route Loaders

路由加载器

Basic Loader

基础加载器

typescript
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await postsApi.getAll();
    return { posts };
  },
  component: PostsPage,
});
typescript
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await postsApi.getAll();
    return { posts };
  },
  component: PostsPage,
});

Loader with Dependencies

带依赖的加载器

typescript
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,
});
typescript
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,
});

Loader Error Handling

加载器错误处理

typescript
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>;
}

typescript
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: '文章未找到' };
    }
  },
  component: PostDetails,
});

function PostDetails() {
  const { post, error } = Route.useLoaderData();

  if (error) return <Error message={error} />;
  return <div>{post.title}</div>;
}

Navigation

导航

typescript
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' } });

typescript
import { Link, useNavigate } from '@tanstack/react-router';

// Link 组件
<Link to="/posts/$postId" params={{ postId: '123' }}>查看文章</Link>

// 编程式导航
const navigate = useNavigate();
navigate({ to: '/posts', search: { filter: 'published' } });

Lazy Loading

懒加载

Lazy Route Component

懒加载路由组件

typescript
// 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,
});
typescript
// 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,
});

Lazy Loader

懒加载加载器

typescript
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,
});

typescript
export const Route = createFileRoute('/posts')({
  loader: async () => {
    // 仅在路由加载时动态导入重型模块
    const { processData } = await import('~/lib/heavyModule');
    const posts = await postsApi.getAll();
    const processed = processData(posts);
    return { posts: processed };
  },
  component: PostsPage,
});

Search Params

查询参数

Type-Safe Search Params

类型安全查询参数

typescript
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>
  );
}
typescript
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>筛选条件: {search.filter}</p>
      <p>排序方式: {search.sort}</p>
      <p>页码: {search.page}</p>
    </div>
  );
}

Updating Search Params

更新查询参数

typescript
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>
  );
}

typescript
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')}>全部</button>
      <button onClick={() => setFilter('published')}>已发布</button>
      <button onClick={() => setFilter('draft')}>草稿</button>
    </div>
  );
}

Layouts

布局

Root Layout

根布局

typescript
// 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>
  );
}
typescript
// routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';

export const Route = createRootRoute({
  component: RootLayout,
});

function RootLayout() {
  return (
    <div>
      <Header />
      <main>
        <Outlet />  {/* 子路由在此渲染 */}
      </main>
      <Footer />
    </div>
  );
}

Nested Layouts

嵌套布局

typescript
// 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,
});

typescript
// routes/dashboard.tsx
export const Route = createFileRoute('/dashboard')({
  component: DashboardLayout,
});

function DashboardLayout() {
  return (
    <div className="dashboard">
      <Sidebar />
      <div className="content">
        <Outlet />  {/* 仪表盘子路由 */}
      </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,
});

Route Guards

路由守卫

Authentication Guard

认证守卫

typescript
export const Route = createFileRoute('/admin')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: '/admin' },
      });
    }
  },
  component: AdminPage,
});
typescript
export const Route = createFileRoute('/admin')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: '/admin' },
      });
    }
  },
  component: AdminPage,
});

Permission Guard

权限守卫

typescript
export const Route = createFileRoute('/admin/users')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.hasPermission('users:manage')) {
      throw redirect({ to: '/unauthorized' });
    }
  },
  component: UsersPage,
});

typescript
export const Route = createFileRoute('/admin/users')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.hasPermission('users:manage')) {
      throw redirect({ to: '/unauthorized' });
    }
  },
  component: UsersPage,
});

Breadcrumbs

面包屑

Route Breadcrumbs

路由面包屑

typescript
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,
});

typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await postsApi.get(params.postId);
    return { post };
  },
  meta: ({ loaderData }) => [
    { title: '首页', path: '/' },
    { title: '文章列表', path: '/posts' },
    { title: loaderData.post.title, path: `/posts/${loaderData.post.id}` },
  ],
  component: PostDetails,
});

Best Practices

最佳实践

1. Use Loaders for Data

1. 使用加载器获取数据

typescript
// ✅ 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>;
}
typescript
// ✅ 推荐: 加载器获取数据
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await postsApi.getAll();
    return { posts };
  },
  component: PostsPage,
});

// ❌ 避免: 在组件内获取数据
function PostsPage() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    postsApi.getAll().then(setPosts);
  }, []);

  return <div>...</div>;
}

2. Lazy Load Heavy Routes

2. 懒加载重型路由

typescript
// ✅ Good: Lazy load admin panel
const AdminPanel = lazy(() => import('~/features/admin/AdminPanel'));

export const Route = createFileRoute('/admin')({
  component: AdminPanel,
});
typescript
// ✅ 推荐: 懒加载管理后台
const AdminPanel = lazy(() => import('~/features/admin/AdminPanel'));

export const Route = createFileRoute('/admin')({
  component: AdminPanel,
});

3. Type-Safe Navigation

3. 类型安全导航

typescript
// ✅ 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>

typescript
// ✅ 推荐: 类型安全的 Link
<Link to="/posts/$postId" params={{ postId: post.id }}>
  查看文章
</Link>

// ❌ 避免: 字符串拼接
<a href={`/posts/${post.id}`}>查看文章</a>

Additional Resources

更多资源

For more patterns, see:
  • routing-guide.md - Advanced routing
  • navigation-patterns.md - Navigation strategies
  • route-loaders.md - Complex loaders
如需了解更多模式,请查看:
  • routing-guide.md - 高级路由
  • navigation-patterns.md - 导航策略
  • route-loaders.md - 复杂加载器