路由进阶 #

一、路由懒加载 #

1.1 基本懒加载 #

javascript
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

1.2 带加载状态的懒加载 #

javascript
function PageLoader() {
  return (
    <div className="page-loader">
      <div className="spinner" />
      <p>加载中...</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          {/* ... */}
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

1.3 预加载 #

javascript
const Dashboard = lazy(() => import('./pages/Dashboard'));

function Navbar() {
  const preloadDashboard = () => {
    import('./pages/Dashboard');
  };

  return (
    <nav>
      <Link
        to="/dashboard"
        onMouseEnter={preloadDashboard}
      >
        Dashboard
      </Link>
    </nav>
  );
}

二、数据加载 #

2.1 使用loader #

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

async function userLoader({ params }) {
  const response = await fetch(`/api/users/${params.id}`);
  if (!response.ok) {
    throw new Response('Not Found', { status: 404 });
  }
  return response.json();
}

const router = createBrowserRouter([
  {
    path: '/users/:id',
    element: <UserDetail />,
    loader: userLoader
  }
]);

function UserDetail() {
  const user = useLoaderData();

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

2.2 嵌套数据加载 #

javascript
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    loader: async () => {
      return { user: await fetchCurrentUser() };
    },
    children: [
      {
        path: 'users',
        element: <UserList />,
        loader: async () => {
          return fetch('/api/users').then(res => res.json());
        }
      },
      {
        path: 'users/:id',
        element: <UserDetail />,
        loader: async ({ params }) => {
          return fetch(`/api/users/${params.id}`).then(res => res.json());
        }
      }
    ]
  }
]);

2.3 使用useRouteLoaderData #

javascript
function UserDetail() {
  const user = useRouteLoaderData('user-detail');

  return (
    <div>
      <h1>{user.name}</h1>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: 'users/:id',
    element: <UserDetail />,
    loader: userLoader,
    id: 'user-detail'
  }
]);

三、错误处理 #

3.1 errorElement #

javascript
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorBoundary />,
    children: [
      {
        path: 'users/:id',
        element: <UserDetail />,
        loader: userLoader,
        errorElement: <UserError />
      }
    ]
  }
]);

3.2 useRouteError #

javascript
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';

function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>出错了</h1>
      <p>{error.message}</p>
    </div>
  );
}

四、路由守卫进阶 #

4.1 高阶路由守卫 #

javascript
function withAuth(WrappedComponent) {
  return function WithAuth() {
    const { user, loading } = useAuth();
    const location = useLocation();

    if (loading) {
      return <Loading />;
    }

    if (!user) {
      return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return <WrappedComponent />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

4.2 权限路由组件 #

javascript
function AuthorizedRoute({ children, permissions = [] }) {
  const { user, hasPermission } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  const hasAccess = permissions.every(p => hasPermission(p));

  if (!hasAccess) {
    return <Navigate to="/forbidden" replace />;
  }

  return children;
}

function App() {
  return (
    <Routes>
      <Route
        path="/admin"
        element={
          <AuthorizedRoute permissions={['admin']}>
            <AdminPanel />
          </AuthorizedRoute>
        }
      />
    </Routes>
  );
}

4.3 动态路由权限 #

javascript
function useAuthorizedRoutes() {
  const { user } = useAuth();

  return useMemo(() => {
    const routes = [...publicRoutes];

    if (user) {
      routes.push(...authenticatedRoutes);

      if (user.role === 'admin') {
        routes.push(...adminRoutes);
      }
    }

    return routes;
  }, [user]);
}

五、路由过渡动画 #

5.1 使用React Transition Group #

javascript
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();

  return (
    <TransitionGroup>
      <CSSTransition
        key={location.pathname}
        classNames="fade"
        timeout={300}
      >
        <Routes location={location}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </CSSTransition>
    </TransitionGroup>
  );
}
css
.fade-enter {
  opacity: 0;
}

.fade-enter-active {
  opacity: 1;
  transition: opacity 300ms;
}

.fade-exit {
  opacity: 1;
}

.fade-exit-active {
  opacity: 0;
  transition: opacity 300ms;
}

5.2 使用Framer Motion #

javascript
import { motion, AnimatePresence } from 'framer-motion';
import { useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={location.pathname}
        initial={{ opacity: 0, x: 100 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: -100 }}
        transition={{ duration: 0.3 }}
      >
        <Routes location={location}>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </motion.div>
    </AnimatePresence>
  );
}

六、路由元信息 #

6.1 页面标题 #

javascript
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

function Home() {
  useDocumentTitle('首页 - My App');
  return <div>Home</div>;
}

// 或使用路由配置
const routes = [
  {
    path: '/',
    element: <Home />,
    handle: { title: '首页' }
  },
  {
    path: '/about',
    element: <About />,
    handle: { title: '关于我们' }
  }
];

function Layout() {
  const route = useMatches();
  const title = route[route.length - 1]?.handle?.title;

  useEffect(() => {
    if (title) {
      document.title = `${title} - My App`;
    }
  }, [title]);

  return <Outlet />;
}

6.2 面包屑导航 #

javascript
const routes = [
  {
    path: '/',
    element: <Layout />,
    handle: { crumb: () => <Link to="/">首页</Link> },
    children: [
      {
        path: 'users',
        element: <UserList />,
        handle: { crumb: () => <Link to="/users">用户</Link> },
        children: [
          {
            path: ':id',
            element: <UserDetail />,
            handle: {
              crumb: ({ params }) => <span>用户 {params.id}</span>
            }
          }
        ]
      }
    ]
  }
];

function Breadcrumbs() {
  const matches = useMatches();
  const crumbs = matches
    .filter(match => match.handle?.crumb)
    .map(match => match.handle.crumb(match.data, match.params));

  return (
    <nav>
      {crumbs.map((crumb, index) => (
        <span key={index}>
          {crumb}
          {index < crumbs.length - 1 && ' > '}
        </span>
      ))}
    </nav>
  );
}

七、路由钩子 #

7.1 useNavigation #

javascript
function SubmitButton() {
  const navigation = useNavigation();

  const isSubmitting = navigation.state === 'submitting';

  return (
    <button disabled={isSubmitting}>
      {isSubmitting ? '提交中...' : '提交'}
    </button>
  );
}

7.2 useBeforeUnload #

javascript
function useBeforeUnload(message) {
  useEffect(() => {
    const handleBeforeUnload = (event) => {
      event.preventDefault();
      event.returnValue = message;
      return message;
    };

    window.addEventListener('beforeunload', handleBeforeUnload);

    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [message]);
}

function EditForm() {
  const [isDirty, setIsDirty] = useState(false);

  useBeforeUnload(isDirty ? '您有未保存的更改,确定要离开吗?' : null);

  return <form>...</form>;
}

7.3 useBlocker #

javascript
import { useBlocker } from 'react-router-dom';

function usePrompt(message, when) {
  const blocker = useBlocker(when);

  useEffect(() => {
    if (blocker.state === 'blocked') {
      const confirmed = window.confirm(message);
      if (confirmed) {
        blocker.proceed();
      } else {
        blocker.reset();
      }
    }
  }, [blocker, message]);
}

function EditForm() {
  const [isDirty, setIsDirty] = useState(false);

  usePrompt('您有未保存的更改,确定要离开吗?', isDirty);

  return <form>...</form>;
}

八、最佳实践 #

8.1 路由配置分离 #

javascript
// routes/index.jsx
export const routes = createRoutes();

// routes/publicRoutes.jsx
export const publicRoutes = [
  { path: '/', element: <Home /> },
  { path: '/login', element: <Login /> }
];

// routes/protectedRoutes.jsx
export const protectedRoutes = [
  {
    path: '/dashboard',
    element: (
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    )
  }
];

8.2 类型安全(TypeScript) #

typescript
import { RouteObject } from 'react-router-dom';

interface CustomRouteObject extends RouteObject {
  auth?: boolean;
  title?: string;
}

const routes: CustomRouteObject[] = [
  {
    path: '/',
    element: <Home />,
    title: '首页'
  },
  {
    path: '/admin',
    element: <Admin />,
    auth: true,
    title: '管理后台'
  }
];

九、总结 #

要点 说明
懒加载 lazy + Suspense
数据加载 loader + useLoaderData
错误处理 errorElement + useRouteError
路由守卫 权限控制组件
过渡动画 TransitionGroup/Framer Motion

核心原则:

  • 使用懒加载优化首屏性能
  • 使用 loader 预加载数据
  • 实现完善的错误处理
  • 合理组织路由配置
最后更新:2026-03-26