Skip to content
刘恩义的技术博客
返回

React Router 6 最佳实践

Edit page

React Router 6 是 React 生态系统中最流行的路由库,它提供了一套完整的路由解决方案,包括客户端路由、服务端路由、嵌套路由等功能。本文将详细介绍 React Router 6 的核心特性、最佳实践与实战案例,帮助你在项目中构建高效、可维护的路由系统。

1. React Router 6 核心特性

1.1 声明式路由

React Router 6 使用声明式语法定义路由,使路由配置更加清晰易懂:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'dashboard',
        element: <Dashboard />,
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

1.2 嵌套路由

React Router 6 简化了嵌套路由的实现,通过 children 属性定义子路由:

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'posts',
        element: <Posts />,
        children: [
          {
            index: true,
            element: <PostList />,
          },
          {
            path: ':id',
            element: <PostDetail />,
          },
        ],
      },
    ],
  },
]);

function Layout() {
  return (
    <div>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/posts">Posts</Link>
      </nav>
      <main>
        <Outlet /> {/* 渲染子路由 */}
      </main>
    </div>
  );
}

1.3 数据加载

React Router 6 引入了 loader 函数,用于在路由渲染前加载数据:

const router = createBrowserRouter([
  {
    path: '/posts/:id',
    element: <PostDetail />,
    loader: async ({ params }) => {
      const response = await fetch(`/api/posts/${params.id}`);
      if (!response.ok) {
        throw new Response('Not Found', { status: 404 });
      }
      return response.json();
    },
  },
]);

function PostDetail() {
  const post = useLoaderData(); // 获取加载的数据
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

1.4 操作与表单处理

React Router 6 提供了 action 函数,用于处理表单提交和其他操作:

const router = createBrowserRouter([
  {
    path: '/posts',
    element: <Posts />,
    action: async ({ request }) => {
      const formData = await request.formData();
      const response = await fetch('/api/posts', {
        method: 'POST',
        body: formData,
      });
      if (!response.ok) {
        throw new Response('Error', { status: 500 });
      }
      return response.json();
    },
  },
]);

function Posts() {
  const navigate = useNavigate();
  const submitAction = useActionData(); // 获取操作结果

  return (
    <div>
      {submitAction && <p>Post created successfully!</p>}
      <form method="post">
        <input type="text" name="title" placeholder="Title" />
        <textarea name="content" placeholder="Content" />
        <button type="submit">Create Post</button>
      </form>
    </div>
  );
}

1.5 错误处理

React Router 6 提供了强大的错误处理机制,通过 errorElement 处理路由错误:

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: 'posts/:id',
        element: <PostDetail />,
        loader: async ({ params }) => {
          const response = await fetch(`/api/posts/${params.id}`);
          if (!response.ok) {
            throw new Response('Not Found', { status: 404 });
          }
          return response.json();
        },
      },
    ],
  },
]);

function ErrorPage() {
  const error = useRouteError();
  return (
    <div>
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>{error instanceof Error ? error.message : 'Unknown error'}</p>
    </div>
  );
}

2. 最佳实践

2.1 路由配置管理

将路由配置集中管理,提高代码可维护性:

// src/routes/index.ts
import { createBrowserRouter } from 'react-router-dom';
import Layout from '@/components/Layout';
import Home from '@/pages/Home';
import About from '@/pages/About';
import Dashboard from '@/pages/Dashboard';
import ErrorPage from '@/pages/ErrorPage';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'dashboard',
        element: <Dashboard />,
        loader: async () => {
          // 加载仪表盘数据
          const data = await fetch('/api/dashboard');
          return data.json();
        },
      },
    ],
  },
]);

export default router;

// src/App.tsx
import { RouterProvider } from 'react-router-dom';
import router from '@/routes';

function App() {
  return <RouterProvider router={router} />;
}

export default App;

2.2 类型安全

使用 TypeScript 增强 React Router 的类型安全性:

// src/types/routes.ts
export type RouteParams = {
  postId: string;
  userId: string;
};

// src/routes/index.ts
import { createBrowserRouter } from 'react-router-dom';
import type { RouteObject } from 'react-router-dom';

const routes: RouteObject[] = [
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        path: 'posts/:postId',
        element: <PostDetail />,
        loader: async ({ params }) => {
          // TypeScript 会推断 params 的类型
          const { postId } = params;
          const response = await fetch(`/api/posts/${postId}`);
          return response.json();
        },
      },
    ],
  },
];

const router = createBrowserRouter(routes);

export default router;

// src/pages/PostDetail.tsx
import { useParams, useLoaderData } from 'react-router-dom';
import type { RouteParams } from '@/types/routes';

function PostDetail() {
  // 类型安全的 params
  const params = useParams<RouteParams>();
  // 类型安全的加载数据
  const post = useLoaderData<Post>();
  
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

2.3 数据加载策略

并行数据加载

使用 Promise.all 并行加载多个数据,提高性能:

const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: <Dashboard />,
    loader: async () => {
      const [user, posts, stats] = await Promise.all([
        fetch('/api/user').then(res => res.json()),
        fetch('/api/posts').then(res => res.json()),
        fetch('/api/stats').then(res => res.json()),
      ]);
      return { user, posts, stats };
    },
  },
]);

延迟数据加载

对于大型应用,可以使用延迟数据加载,减少初始加载时间:

const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: <Dashboard />,
    loader: async () => {
      // 只加载关键数据
      const user = await fetch('/api/user').then(res => res.json());
      return { user };
    },
  },
]);

// 在组件中使用 useEffect 加载非关键数据
function Dashboard() {
  const { user } = useLoaderData();
  const [posts, setPosts] = useState([]);
  const [stats, setStats] = useState({});
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchAdditionalData = async () => {
      const [postsData, statsData] = await Promise.all([
        fetch('/api/posts').then(res => res.json()),
        fetch('/api/stats').then(res => res.json()),
      ]);
      setPosts(postsData);
      setStats(statsData);
      setLoading(false);
    };
    
    fetchAdditionalData();
  }, []);
  
  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      {loading ? (
        <p>Loading additional data...</p>
      ) : (
        <>
          <PostsList posts={posts} />
          <Stats stats={stats} />
        </>
      )}
    </div>
  );
}

2.4 权限管理

实现基于角色的权限管理,控制路由访问:

// src/components/AuthGuard.tsx
import { Navigate, useLoaderData } from 'react-router-dom';

interface AuthGuardProps {
  children: React.ReactNode;
  requiredRole?: 'admin' | 'user' | 'guest';
}

function AuthGuard({ children, requiredRole = 'user' }: AuthGuardProps) {
  const { user, loading } = useLoaderData<{ user: User | null; loading: boolean }>();
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  
  if (requiredRole === 'admin' && user.role !== 'admin') {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return <>{children}</>;
}

// src/routes/index.ts
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    loader: async () => {
      try {
        const response = await fetch('/api/user');
        if (response.ok) {
          const user = await response.json();
          return { user, loading: false };
        }
      } catch (error) {
        console.error('Error fetching user:', error);
      }
      return { user: null, loading: false };
    },
    children: [
      {
        path: 'admin',
        element: (
          <AuthGuard requiredRole="admin">
            <AdminPanel />
          </AuthGuard>
        ),
      },
      {
        path: 'dashboard',
        element: (
          <AuthGuard>
            <Dashboard />
          </AuthGuard>
        ),
      },
    ],
  },
]);

2.5 导航与状态管理

编程式导航

使用 useNavigate 钩子实现编程式导航:

import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();
  const [error, setError] = useState('');
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const email = formData.get('email') as string;
    const password = formData.get('password') as string;
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });
      
      if (response.ok) {
        // 登录成功,导航到仪表盘
        navigate('/dashboard', { replace: true });
      } else {
        const data = await response.json();
        setError(data.message || 'Login failed');
      }
    } catch (error) {
      setError('An error occurred');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {error && <p className="error">{error}</p>}
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</button>
    </form>
  );
}

导航状态

使用 state 属性传递导航状态:

import { useNavigate, useLocation } from 'react-router-dom';

function ProductList() {
  const navigate = useNavigate();
  
  const handleProductClick = (product: Product) => {
    navigate(`/products/${product.id}`, {
      state: {
        from: '/products',
        productName: product.name,
      },
    });
  };
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id} onClick={() => handleProductClick(product)}>
          {product.name}
        </div>
      ))}
    </div>
  );
}

function ProductDetail() {
  const location = useLocation();
  const productName = location.state?.productName;
  
  return (
    <div>
      <h1>{productName}</h1>
      {/* 产品详情 */}
    </div>
  );
}

3. 实战案例

3.1 电商应用路由

功能需求

路由配置

// src/routes/index.ts
import { createBrowserRouter } from 'react-router-dom';
import Layout from '@/components/Layout';
import Home from '@/pages/Home';
import ProductList from '@/pages/ProductList';
import ProductDetail from '@/pages/ProductDetail';
import Cart from '@/pages/Cart';
import Checkout from '@/pages/Checkout';
import UserCenter from '@/pages/UserCenter';
import OrderHistory from '@/pages/OrderHistory';
import OrderDetail from '@/pages/OrderDetail';
import Login from '@/pages/Login';
import Register from '@/pages/Register';
import ErrorPage from '@/pages/ErrorPage';
import AuthGuard from '@/components/AuthGuard';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />,
        loader: async () => {
          const response = await fetch('/api/products/recommended');
          return response.json();
        },
      },
      {
        path: 'products',
        children: [
          {
            index: true,
            element: <ProductList />,
            loader: async ({ request }) => {
              const url = new URL(request.url);
              const category = url.searchParams.get('category');
              const response = await fetch(`/api/products?category=${category || ''}`);
              return response.json();
            },
          },
          {
            path: ':id',
            element: <ProductDetail />,
            loader: async ({ params }) => {
              const { id } = params;
              const [product, reviews] = await Promise.all([
                fetch(`/api/products/${id}`).then(res => res.json()),
                fetch(`/api/products/${id}/reviews`).then(res => res.json()),
              ]);
              return { product, reviews };
            },
          },
        ],
      },
      {
        path: 'cart',
        element: <Cart />,
      },
      {
        path: 'checkout',
        element: (
          <AuthGuard>
            <Checkout />
          </AuthGuard>
        ),
      },
      {
        path: 'user',
        element: (
          <AuthGuard>
            <UserCenter />
          </AuthGuard>
        ),
        children: [
          {
            index: true,
            element: <UserProfile />,
          },
          {
            path: 'orders',
            children: [
              {
                index: true,
                element: <OrderHistory />,
                loader: async () => {
                  const response = await fetch('/api/orders');
                  return response.json();
                },
              },
              {
                path: ':id',
                element: <OrderDetail />,
                loader: async ({ params }) => {
                  const { id } = params;
                  const response = await fetch(`/api/orders/${id}`);
                  return response.json();
                },
              },
            ],
          },
        ],
      },
      {
        path: 'login',
        element: <Login />,
      },
      {
        path: 'register',
        element: <Register />,
      },
    ],
  },
]);

export default router;

3.2 博客应用路由

功能需求

路由配置

// src/routes/index.ts
import { createBrowserRouter } from 'react-router-dom';
import Layout from '@/components/Layout';
import AdminLayout from '@/components/AdminLayout';
import Home from '@/pages/Home';
import PostList from '@/pages/PostList';
import PostDetail from '@/pages/PostDetail';
import CategoryList from '@/pages/CategoryList';
import TagList from '@/pages/TagList';
import AdminDashboard from '@/pages/admin/Dashboard';
import PostCreate from '@/pages/admin/PostCreate';
import PostEdit from '@/pages/admin/PostEdit';
import CategoryManage from '@/pages/admin/CategoryManage';
import TagManage from '@/pages/admin/TagManage';
import Login from '@/pages/Login';
import ErrorPage from '@/pages/ErrorPage';
import AuthGuard from '@/components/AuthGuard';
import AdminGuard from '@/components/AdminGuard';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />,
        loader: async () => {
          const response = await fetch('/api/posts/latest');
          return response.json();
        },
      },
      {
        path: 'posts',
        children: [
          {
            index: true,
            element: <PostList />,
            loader: async ({ request }) => {
              const url = new URL(request.url);
              const page = url.searchParams.get('page') || '1';
              const response = await fetch(`/api/posts?page=${page}`);
              return response.json();
            },
          },
          {
            path: ':slug',
            element: <PostDetail />,
            loader: async ({ params }) => {
              const { slug } = params;
              const [post, comments] = await Promise.all([
                fetch(`/api/posts/${slug}`).then(res => res.json()),
                fetch(`/api/posts/${slug}/comments`).then(res => res.json()),
              ]);
              return { post, comments };
            },
          },
        ],
      },
      {
        path: 'categories',
        children: [
          {
            index: true,
            element: <CategoryList />,
            loader: async () => {
              const response = await fetch('/api/categories');
              return response.json();
            },
          },
          {
            path: ':slug',
            element: <PostList />,
            loader: async ({ params, request }) => {
              const { slug } = params;
              const url = new URL(request.url);
              const page = url.searchParams.get('page') || '1';
              const response = await fetch(`/api/posts?category=${slug}&page=${page}`);
              return response.json();
            },
          },
        ],
      },
      {
        path: 'tags',
        children: [
          {
            index: true,
            element: <TagList />,
            loader: async () => {
              const response = await fetch('/api/tags');
              return response.json();
            },
          },
          {
            path: ':slug',
            element: <PostList />,
            loader: async ({ params, request }) => {
              const { slug } = params;
              const url = new URL(request.url);
              const page = url.searchParams.get('page') || '1';
              const response = await fetch(`/api/posts?tag=${slug}&page=${page}`);
              return response.json();
            },
          },
        ],
      },
      {
        path: 'login',
        element: <Login />,
      },
    ],
  },
  {
    path: '/admin',
    element: (
      <AdminGuard>
        <AdminLayout />
      </AdminGuard>
    ),
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <AdminDashboard />,
        loader: async () => {
          const [posts, categories, tags] = await Promise.all([
            fetch('/api/admin/posts/count').then(res => res.json()),
            fetch('/api/admin/categories/count').then(res => res.json()),
            fetch('/api/admin/tags/count').then(res => res.json()),
          ]);
          return { posts, categories, tags };
        },
      },
      {
        path: 'posts',
        children: [
          {
            index: true,
            element: <AdminPostList />,
            loader: async ({ request }) => {
              const url = new URL(request.url);
              const page = url.searchParams.get('page') || '1';
              const response = await fetch(`/api/admin/posts?page=${page}`);
              return response.json();
            },
          },
          {
            path: 'create',
            element: <PostCreate />,
          },
          {
            path: 'edit/:id',
            element: <PostEdit />,
            loader: async ({ params }) => {
              const { id } = params;
              const response = await fetch(`/api/admin/posts/${id}`);
              return response.json();
            },
          },
        ],
      },
      {
        path: 'categories',
        element: <CategoryManage />,
        loader: async () => {
          const response = await fetch('/api/admin/categories');
          return response.json();
        },
      },
      {
        path: 'tags',
        element: <TagManage />,
        loader: async () => {
          const response = await fetch('/api/admin/tags');
          return response.json();
        },
      },
    ],
  },
]);

export default router;

3.2 管理系统路由

功能需求

路由配置

// src/routes/index.ts
import { createBrowserRouter } from 'react-router-dom';
import AdminLayout from '@/components/AdminLayout';
import Dashboard from '@/pages/admin/Dashboard';
import UserList from '@/pages/admin/UserList';
import UserEdit from '@/pages/admin/UserEdit';
import RoleList from '@/pages/admin/RoleList';
import RoleEdit from '@/pages/admin/RoleEdit';
import ContentList from '@/pages/admin/ContentList';
import ContentEdit from '@/pages/admin/ContentEdit';
import SystemSettings from '@/pages/admin/SystemSettings';
import Login from '@/pages/Login';
import ErrorPage from '@/pages/ErrorPage';
import AdminGuard from '@/components/AdminGuard';

const router = createBrowserRouter([
  {
    path: '/login',
    element: <Login />,
  },
  {
    path: '/admin',
    element: (
      <AdminGuard>
        <AdminLayout />
      </AdminGuard>
    ),
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Dashboard />,
        loader: async () => {
          const [users, roles, content] = await Promise.all([
            fetch('/api/admin/users/count').then(res => res.json()),
            fetch('/api/admin/roles/count').then(res => res.json()),
            fetch('/api/admin/content/count').then(res => res.json()),
          ]);
          return { users, roles, content };
        },
      },
      {
        path: 'users',
        children: [
          {
            index: true,
            element: <UserList />,
            loader: async ({ request }) => {
              const url = new URL(request.url);
              const page = url.searchParams.get('page') || '1';
              const response = await fetch(`/api/admin/users?page=${page}`);
              return response.json();
            },
          },
          {
            path: 'edit/:id',
            element: <UserEdit />,
            loader: async ({ params }) => {
              const { id } = params;
              const response = await fetch(`/api/admin/users/${id}`);
              return response.json();
            },
          },
        ],
      },
      {
        path: 'roles',
        children: [
          {
            index: true,
            element: <RoleList />,
            loader: async () => {
              const response = await fetch('/api/admin/roles');
              return response.json();
            },
          },
          {
            path: 'edit/:id',
            element: <RoleEdit />,
            loader: async ({ params }) => {
              const { id } = params;
              const response = await fetch(`/api/admin/roles/${id}`);
              return response.json();
            },
          },
        ],
      },
      {
        path: 'content',
        children: [
          {
            index: true,
            element: <ContentList />,
            loader: async ({ request }) => {
              const url = new URL(request.url);
              const page = url.searchParams.get('page') || '1';
              const response = await fetch(`/api/admin/content?page=${page}`);
              return response.json();
            },
          },
          {
            path: 'edit/:id',
            element: <ContentEdit />,
            loader: async ({ params }) => {
              const { id } = params;
              const response = await fetch(`/api/admin/content/${id}`);
              return response.json();
            },
          },
        ],
      },
      {
        path: 'settings',
        element: <SystemSettings />,
        loader: async () => {
          const response = await fetch('/api/admin/settings');
          return response.json();
        },
      },
    ],
  },
]);

export default router;

4. 最佳实践总结

4.1 路由设计

4.2 数据加载

4.3 权限管理

4.4 性能优化

4.5 开发体验

总结

React Router 6 提供了一套完整、灵活的路由解决方案,通过本文介绍的核心特性、最佳实践与实战案例,你可以:

  1. 构建声明式路由:使用 createBrowserRouterRouterProvider 定义路由
  2. 实现嵌套路由:通过 children 属性组织路由结构
  3. 优化数据加载:使用 loader 函数在路由渲染前加载数据
  4. 处理表单提交:使用 action 函数处理表单提交和其他操作
  5. 实现权限管理:通过路由守卫控制路由访问
  6. 优化用户体验:使用编程式导航和导航状态

随着 React 生态系统的不断发展,React Router 也在持续改进,为开发者提供更好的路由解决方案。掌握 React Router 6 的最佳实践,将帮助你构建更加高效、可维护的 React 应用。


Edit page

Previous Post
React 状态管理最佳实践
Next Post
React Server Components 实战指南