博客 / 詳情

返回

掌握 React Router:構建你的 React 應用導航

大家好,我是長林啊!一個 Go、Rust 愛好者,同時也是一名全棧開發者;致力於終生學習和技術分享。

本文首發於微信公眾號《全棧修煉之旅》,歡迎大家關注!

在構建現代 Web 應用時,導航是連接用户界面的關鍵紐帶。React Router 作為 React 生態中的核心路由庫,為開發者提供了強大的工具來實現 SPA(單頁應用)的導航邏輯。它不僅簡化了頁面間的跳轉,還支持動態路由匹配、懶加載和狀態管理集成,讓應用的導航更加靈活和高效。

初識 React Router

React Router 是一個用於 React 應用程序的路由庫,它允許你以聲明式的方式來定義應用的導航結構。

介紹 React Router的重要性

React Router 的重要性在於它為構建單頁應用(SPA)提供了一個強大而靈活的導航解決方案。以下是 React Router 的幾個關鍵重要性點:

  • 用户體驗:React Router 允許應用在不重新加載頁面的情況下進行頁面跳轉,提供了無縫的用户體驗。
  • 應用結構:它幫助開發者以組件化的方式組織應用的視圖,使得應用的結構更加清晰和模塊化。
  • 動態路由:React Router 支持動態路由,可以根據URL參數動態渲染組件,這在處理用户輸入和API數據時非常有用。
  • 導航控制:提供了編程式導航和聲明式導航的方式,使得開發者可以更靈活地控制應用的導航流程。
  • 狀態同步:React Router 能夠與 React 的狀態管理庫(如Redux或Context API)集成,同步路由狀態與應用狀態。
  • 性能優化:通過懶加載和代碼分割,React Router 有助於提高應用的加載速度和運行效率。
  • SEO友好:對於需要進行搜索引擎優化的應用,React Router 支持服務器端渲染,有助於提高SEO效果。
  • 社區支持:React Router 有着龐大的社區支持,提供了大量的教程、插件和第三方集成方案。
  • 安全性:React Router 提供了路由保護機制,可以防止未授權的路由訪問,增強應用的安全性。
  • 跨平台兼容性:React Router 不僅限於 Web 應用,還可以與 React Native 等其他React平台集成,提供跨平台的導航解決方案。

    概述 React Router 在現代Web應用中的作用

  • 增強用户體驗:通過實現無縫頁面跳轉,React Router提升了用户交互的流暢性。
  • 促進代碼組織:它通過組件化路由,幫助開發者以模塊化的方式組織代碼,提高應用的可維護性。
  • 支持動態內容:React Router允許根據URL動態加載內容,為構建數據驅動的應用提供了便利。
  • 提高性能:通過懶加載和代碼分割,它有助於減少初始加載時間和提高應用性能。
  • 保障安全性:提供了路由保護功能,確保應用的導航邏輯安全且符合業務規則。
  • 改善SEO:支持服務器端渲染,有助於提高應用的搜索引擎優化效果。
  • 靈活集成:React Router可以與多種狀態管理和UI庫集成,提供一致的開發體驗。

下文有不少示例演示,我們就用 vite 創建一個新的 React 項目吧!創建的命令如下:

$ npm create vite@latest react-router-tutorial -- --template react

創建完成之後,用自己熟悉的 IDE 工具打開,並在終端中運行命令啓動項目。

安裝 React Router

可以選擇自己熟悉的 Node.js 包管理工具,建議在同一個項目中只使用一種包管理工具,混合使用可能會導致一些包依賴出問題;建議直接使用 pnpm 包管理工具。

$ pnpm add react-router-dom

路由

React Router 提供了多種創建路由的方式,在 v6.4 又引入了4中新的創建路由的方式:

  • createBrowserRouter 它使用 DOM History API 來更新 URL 並管理歷史記錄堆棧。
  • createMemoryRouter
  • createHashRouter
  • createStaticRouter
    當然原來聲明式的創建路由的方式仍然還是保留了:
  • <BrowserRouter>
  • <MemoryRouter>
  • <HashRouter>
  • <NativeRouter>
  • <StaticRouter>
    這四種聲明式的創建路由不支持 react-router 新增的一些 Data 相關的 API 的使用,官方也建議所有 Web 項目使用 createBrowserRouter 的方式創建路由。

創建路由的方法詳解

createBrowserRouter 的使用

它還支持 v6.4 數據 API,如 loaders、actions、fetchers 等。

const router = createBrowserRouter([
  {
    path: "/",
    element: <ComponentA />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <ComponentB />,
        loader: teamLoader,
      },
    ],
  },
]);

createBrowserRouter 的類型:

function createBrowserRouter (
    routes: RouteObject[],
    opts?: {
        basename?: string;
        future?: FutureConfig;
        hydrationData?: HydrationState;
        window?: Window;
    }
): RemixRouter;
  • routes:Route 對象的數組,在 children 屬性上有嵌套路由。
  • basename:應用程序的基名,用於無法部署到域根目錄而只能部署到子目錄的情況。

    createBrowserRouter(routes, {
        basename: "/app",
    });
  • future:為路由器啓用的一組可選的 Future Flags

    const router = createBrowserRouter(routes, {
        future: {
            // Normalize `useNavigation()`/`useFetcher()` `formMethod` to uppercase
            v7_normalizeFormMethod: true,
        },
    });

    目前可用的 future flags 如下:

    Flag Description 説明
    v7_fetcherPersist 延遲活動的 fetcher 清理,直到它們返回到 idle 狀態
    v7_normalizeFormMethod useNavigation().formMethod 規範化為大寫的 HTTP 方法
    v7_partialHydration 支持服務端渲染應用程序的部分水合功能
    v7_prependBasename 將路由的基名添加到 navigate/fetch 路徑的前面
    v7_relativeSplatPath 修復 splat 路由中相對路徑解析的錯誤
  • hydrationData:在進行服務器渲染並選擇退出自動水合時, hydrationData 選項允許您從服務器渲染器中傳遞水合數據。

    const router = createBrowserRouter(routes, {
        hydrationData: {
            loaderData: {
                // [routeId]: serverLoaderData
            },
            // may also include `errors` and/or `actionData`
        },
    });
  • window:對於瀏覽器 devtool 插件或測試等環境來説,使用與全局 window 不同的窗口非常有用。

createHashRouter

如果您無法配置 Web 服務器以將所有流量導向 React Router 應用程序,則此路由器非常有用。

不建議使用 Hash 路由!

功能與 createBrowserRouter 並無二致。

const router = createHashRouter([
  {
    path: "/",
    element: <ComponentA />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <ComponentB />,
        loader: teamLoader,
      },
    ],
  },
]);

createMemoryRouter

內存路由器不使用瀏覽器的歷史記錄,而是在內存中管理自己的歷史記錄堆棧。它主要用於測試和組件開發工具(如 Storybook),但也可用於在任何非瀏覽器環境中運行 React Router。

import * as React from "react";
import {
  RouterProvider,
  createMemoryRouter,
} from "react-router-dom";
import {
  render,
  waitFor,
  screen,
} from "@testing-library/react";
import "@testing-library/jest-dom";
import CalendarEvent from "./routes/event";

test("event route", async () => {
  const FAKE_EVENT = { name: "test event" };
  const routes = [
    {
      path: "/events/:id",
      element: <CalendarEvent />,
      loader: () => FAKE_EVENT,
    },
  ];

  const router = createMemoryRouter(routes, {
    initialEntries: ["/", "/events/123"], // 歷史記錄
    initialIndex: 1, // 初始化索引
  });

  render(<RouterProvider router={router} />);

  await waitFor(() => screen.getByRole("heading"));
  expect(screen.getByRole("heading")).toHaveTextContent(
    FAKE_EVENT.name
  );
});

參數除了 initialIndexinitialEntries 外其它參數與 createBorwserRouter 並無二致。

  • initialEntries:歷史記錄堆棧中的初始條目。可以使用歷史記錄堆棧中已有的多個位置來啓動測試(或應用)(用於測試後退導航等)

    createMemoryRouter(routes, {
        initialEntries: ["/", "/events/123"],
    });
  • initialIndex:歷史堆棧中要呈現的初始索引。從特定條目開始測試。它默認為 中的最後一個條目 initialEntries

    createMemoryRouter(routes, {
      initialEntries: ["/", "/events/123"],
      initialIndex: 1, // start at "/events/123"
    });

createStaticHandler

createStaticHandler 用於在服務器端通過 呈現應用程序之前在服務器(即 Node <StaticRouterProvider> 或其他 Javascript 運行時)上執行數據獲取和提交。

import {
  createStaticHandler,
  createStaticRouter,
  StaticRouterProvider,
} from "react-router-dom/server";

// ...

const routes = [
  {
    path: "/",
    loader: exampleLoader,
    Component: Root,
    ErrorBoundary: ComponentA,
  },
];

export async function renderHtml(req) {
  let { query, dataRoutes } = createStaticHandler(routes);
  let fetchRequest = createFetchRequest(req);
  let context = await query(fetchRequest);

  // If we got a redirect response, short circuit and let our Express server
  // handle that directly
  if (context instanceof Response) {
    throw context;
  }

  let router = createStaticRouter(dataRoutes, context);
  return ReactDOMServer.renderToString(
    <React.StrictMode>
      <StaticRouterProvider
        router={router}
        context={context}
      />
    </React.StrictMode>
  );
}

routesbasenamecreateBrowserRouter 是一樣的,

  • handler.query() 方法接受 Fetch 請求,執行路由匹配,並根據請求執行所有相關的路由 action/loader 方法,返回context值包含呈現請求的 HTML 文檔所需的所有信息(路由級別 actionDataloaderDataerrors 等)。如果任何匹配的路由返回或拋出重定向響應,query() 則將以 Fetch 的形式返回該重定向 Response。如果請求被中止,query 將拋出錯誤,例如 Error("query() call aborted: GET /path")。如果你想拋出本機 AbortSignal.reason(默認情況下為 DOMException),你可以選擇加入 future.v7_throwAbortReason 未來標誌。

    DOMException 是在 Node 17 中添加的,因此你必須在 Node 17 或更高版本上才能正常工作。
  • opts.requestContext 如果您需要將信息從服務器傳遞到 Remix action/loader,您可以使用它來執行此操作,opts.requestContext 它將顯示在上下文參數中的操作/加載器中。

    const routes = [{
      path: '/',
      loader({ request, context }) {
        // Access `context.dataFormExpressMiddleware` here
      },
    }];
    
    export async function render(req: express.Request) {
      let { query, dataRoutes } = createStaticHandler(routes);
      let remixRequest = createFetchRequest(request);
      let staticHandlerContext = await query(remixRequest, {
        // Pass data from the express layer to the remix layer here
        requestContext: {
          dataFromExpressMiddleware: req.something
        }
     });
     ...
    }
  • opts.routeId

    如果你需要調用一個與 URL 不完全對應的特定路由操作/加載器(例如,父路由加載器),你可以指定routeId

    staticHandler.queryRoute(new Request("/parent/child"), {
      routeId: "parent",
    });
  • opts.requestContext

    如果您需要將信息從服務器傳遞到 Remix action/loader 中,您可以使用 進行傳遞,opts.requestContext 它將顯示在上下文參數中的 action/loader 中。

createStaticRouter

createStaticRouter 是利用數據路由器在服務器(即Node或其他 Javascript 運行時)上進行渲染時,可以使用它。

import {
  createStaticHandler,
  createStaticRouter,
  StaticRouterProvider,
} from "react-router-dom/server";

// ...

const routes = [
  {
    path: "/",
    loader: exampleLoader,
    Component: ComponentA,
    ErrorBoundary: RootErrorBoundary,
  },
];

export async function renderHtml(req) {
  let { query, dataRoutes } = createStaticHandler(routes);
  let fetchRequest = createFetchRequest(req);
  let context = await query(fetchRequest);

  // If we got a redirect response, short circuit and let our Express server
  // handle that directly
  if (context instanceof Response) {
    throw context;
  }

  let router = createStaticRouter(dataRoutes, context);
  return ReactDOMServer.renderToString(
    <React.StrictMode>
      <StaticRouterProvider
        router={router}
        context={context}
      />
    </React.StrictMode>
  );
}

RouterProvider

RouterProvider 是 react-router-dom 提供的一個組件,用於將路由配置傳遞給整個應用,使得應用中的所有組件都可以訪問和使用這些路由信息。

主要功能:

  • 提供路由上下文:RouterProvider 創建並提供一個路由上下文,使應用中的任何組件都可以方便地訪問路由信息和導航功能。
  • 管理路由狀態:它負責管理路由的狀態和更新,包括當前路徑、導航歷史等。
  • 加載數據:結合路由加載器 (loader),RouterProvider 可以在路由切換時預加載數據,提高應用的性能和用户體驗。

以下是一個完整的示例,展示瞭如何使用 RouterProvider 配置和提供路由:

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

// 定義組件
function Home() {
  return <h2>Home Page</h2>;
}

function About() {
  return <h2>About Page</h2>;
}

// 創建路由實例
const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
  },
  {
    path: "/about",
    element: <About />,
  },
]);

// 提供路由上下文
ReactDOM.createRoot(document.getElementById('root')).render(
  <RouterProvider router={router} />
);

除了在上面的示例中用到的 router 參數外,還有 fallbaclElement,它用於在路由加載過程中顯示一個備用的 UI 元素。這在需要加載數據或組件時非常有用,可以提供一個良好的用户體驗,例如顯示一個加載指示器或佔位符,直到實際的內容加載完畢。例如:

<RouterProvider
  router={router}
  fallbackElement={<SpinnerOfDoom />}
/>

StaticRouterProvider

StaticRouterProvider 是 react-router-dom 提供的一個組件,主要用於在服務器端渲染(SSR)環境中進行路由配置。與客户端渲染不同,服務器端渲染需要在服務器上執行路由匹配和組件渲染,然後將渲染好的 HTML 發送到客户端。

主要用途

  • 服務器端渲染(SSR):StaticRouterProvider 適合在服務器端環境中使用,配合 React 的服務器端渲染功能,實現同構應用。
  • 靜態路由配置:它使用靜態路由配置,不依賴於瀏覽器的歷史記錄和導航功能。
    主要特性
  • 靜態上下文:StaticRouterProvider 使用靜態上下文進行路由匹配,這在服務器端環境中是必要的,因為沒有瀏覽器的歷史記錄或導航功能。
  • 便於 SSR:它簡化了在服務器端進行路由匹配和渲染的過程。
import {
  createStaticHandler,
  createStaticRouter,
  StaticRouterProvider,
} from "react-router-dom/server";

const routes = [
  {
    path: "/",
    loader: rootLoader,
    Component: Root,
    ErrorBoundary: RootBoundary,
  },
];

export async function renderHtml(req) {
  let { query, dataRoutes } = createStaticHandler(routes);
  let fetchRequest = createFetchRequest(req);
  let context = await query(fetchRequest);

  // If we got a redirect response, short circuit and let our Express server
  // handle that directly
  if (context instanceof Response) {
    throw context;
  }

  let router = createStaticRouter(dataRoutes, context);
  return ReactDOMServer.renderToString(
    <React.StrictMode>
      <StaticRouterProvider
        router={router}
        context={context}
      />
    </React.StrictMode>
  );
}

這個方法的主要參數:

  • context:從createStaticHandler().query()調用返回的內容,其中包含了請求所獲取的所有數據。
  • router:這是通過以下方式創建的路由器 createStaticRouter
  • hydrate:默認情況下,<StaticRouterProvider> 將把所需的水合數據字符串化到標籤 window.__staticRouterHydrationData <script>,該標籤將被讀取並自動水合 createBrowserRouter()。如果希望手動進行更高級的水合,您可以通過 來 hydrate={false} 禁用此自動水合。在客户端,您可以將自己的水合傳遞 hydrationDatacreateBrowserRouter
  • nonce:當利用自動補水時,您可以提供一個 nonce 要呈現到 <script> 標籤上並與內容安全策略一起使用的值。

路由組件

BrowserRouter(瀏覽器組件)

將當前位置存儲在瀏覽器的地址欄中,並使用瀏覽器的內置歷史記錄堆棧進行導航。

declare function BrowserRouter(
    props: BrowserRouterProps
): React.ReactElement;

interface BrowserRouterProps {
    basename?: string;
    children?: React.ReactNode;
    future?: FutureConfig;
    window?: Window;
}

參數解釋

  • windowBrowserRouter 默認使用當前文檔的 defaultView,但它也可用於跟蹤另一個窗口的 URL 的變化,例如 <iframe>
  • future:一組可選的未來標誌可供啓用。

    function App() {
      return (
        <BrowserRouter future={{ v7_startTransition: true }}>
          <Routes>{/*...*/}</Routes>
        </BrowserRouter>
      );
    }
  • basename:一個用於指定基礎路徑的屬性。它主要用於當你的應用部署在一個子路徑(而不是根路徑)時,確保路由能夠正確匹配和導航。

    主要用途

    1. 支持子路徑部署:當你的應用部署在非根路徑(例如 https://example.com/myapp)時,basename 可以幫助你的應用正確處理路由。
    2. 確保 URL 正確:通過設置 basename,你可以確保所有鏈接和導航行為都基於指定的基礎路徑。
    import * as React from "react";
    import * as ReactDOM from "react-dom";
    import { createBrowserRouter, RouterProvider, Link, Outlet } from 'react-router-dom';
    
    // 定義組件
    function Home() {
      return <h2>Home Page</h2>;
    }
    
    function About() {
      return <h2>About Page</h2>;
    }
    
    function App() {
      return (
        <div>
          <nav>
            <Link to="/">Home</Link>
            <Link to="/about">About</Link>
          </nav>
          <Outlet />
        </div>
      );
    }
    
    // 創建路由器實例並設置 basename
    const router = createBrowserRouter(
      [
        {
          path: "/",
          element: <App />,
          children: [
            {
              path: "/",
              element: <Home />,
            },
            {
              path: "/about",
              element: <About />,
            },
          ],
        },
      ],
      {
        basename: "/myapp",  // 設置基礎路徑
      }
    );
    
    // 渲染應用
    ReactDOM.createRoot(document.getElementById("root")).render(
      <RouterProvider router={router} />
    );

    在這個示例中,假設你的應用部署在 https://example.com/myapp,設置了 basename: "/myapp" 後,Link 組件和路由匹配都會基於這個基礎路徑進行。導航到 /about 實際上會導航到 https://example.com/myapp/about。

HashRouter(哈希路由)

在 react-router-dom 中,HashRouter 是一種用於管理路由的組件,它使用 URL 的哈希部分(即 # 後面的部分)來保持 UI 與 URL 同步。HashRouter 適用於那些服務器端不處理路由的應用,例如靜態文件服務,或在某些需要兼容舊版本瀏覽器的場景中。

declare function HashRouter(
  props: HashRouterProps
): React.ReactElement;

interface HashRouterProps {
  basename?: string;
  children?: React.ReactNode;
  future?: FutureConfig;
  window?: Window;
}

主要用途

  • 兼容性:HashRouter 可以在不支持 HTML5 歷史記錄 API 的舊版本瀏覽器中使用。
  • 簡單部署:當你的服務器不處理路由時,HashRouter 是一個簡便的解決方案,因為它不需要服務器端的配置來支持不同的 URL。

基本特性

  • 使用 URL 哈希部分進行路由。
  • URL 形式為 http://example.com/#/your/path。
  • 哈希部分的變化不會觸發服務器請求,僅會觸發客户端的路由變化。
import { HashRouter, Route, Routes, Link } from 'react-router-dom';

// 定義組件
function Home() {
  return <h2>Home Page</h2>;
}

function About() {
  return <h2>About Page</h2>;
}

function App() {
  return (
    <div>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
}

// 使用 HashRouter 包裹應用
ReactDOM.createRoot(document.getElementById('root')).render(
  <HashRouter>
    <App />
  </HashRouter>
);

在這個示例中,HashRouter 使用 URL 的哈希部分進行路由,所以當你點擊導航鏈接時,URL 會變為 http://example.com/#/about 或 http://example.com/#/,而不需要服務器進行任何配置或處理。

MemoryRouter

<MemoryRouter> 將其位置存儲在數組內部。與 <BrowserHistory><HashHistory> 不同它不依賴於外部源,例如瀏覽器中的歷史記錄堆棧。這使其成為需要完全控制歷史記錄堆棧的場景(例如測試)的理想選擇。

declare function MemoryRouter(
  props: MemoryRouterProps
): React.ReactElement;

interface MemoryRouterProps {
  basename?: string;
  children?: React.ReactNode;
  initialEntries?: InitialEntry[];
  initialIndex?: number;
  future?: FutureConfig;
}

這個組件一般用於寫單元測試,比如:

import * as React from "react";
import { create } from "react-test-renderer";
import {
  MemoryRouter,
  Routes,
  Route,
} from "react-router-dom";

describe("My app", () => {
  it("renders correctly", () => {
    let renderer = create(
      <MemoryRouter initialEntries={["/users/mjackson"]}>
        <Routes>
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserProfile />} />
          </Route>
        </Routes>
      </MemoryRouter>
    );

    expect(renderer.toJSON()).toMatchSnapshot();
  });
});

NativeRouter 組件實在 React Native 中使用的路由導航的工具,就不在本系列文章中涉及;因為 React Native 也是一個比較大的概念。

Route

Route 組件

Route 是 react-router-dom 中的一個核心組件,用於定義應用中的各個路由及其關聯的組件。它的主要作用是根據當前的 URL 匹配相應的路徑,並渲染對應的組件。

主要特性

  • 路徑匹配:Route 組件根據指定的路徑(path)來匹配 URL。
  • 組件渲染:當路徑匹配成功時,渲染對應的組件。
  • 嵌套路由:支持嵌套路由,通過 Outlet 組件實現子路由的嵌套渲染。

基本用法
創建 Route 有兩種方式,一種是通過 createBrowserRouter 的方式創建,另一種是通過聲明式的方式創建,如下:

  • createBrowserRouter 函數的方式創建

    const router = createBrowserRouter([
      {
        // it renders this element
        element: <Team />,
    
        // when the URL matches this segment
        path: "teams/:teamId",
    
        // with this data loaded before rendering
        loader: async ({ request, params }) => {
          return fetch(
            `/fake/api/teams/${params.teamId}.json`,
            { signal: request.signal }
          );
        },
    
        // performing this mutation when data is submitted to it
        action: async ({ request }) => {
          return updateFakeTeam(await request.formData());
        },
    
        // and renders this element in case something went wrong
        errorElement: <ErrorBoundary />,
      },
    ]);
  • 聲明式創建

    const router = createBrowserRouter(
      createRoutesFromElements(
        <Route
          element={<Team />}
          path="teams/:teamId"
          loader={async ({ params }) => {
            return fetch(
              `/fake/api/teams/${params.teamId}.json`
            );
          }}
          action={async ({ request }) => {
            return updateFakeTeam(await request.formData());
          }}
          errorElement={<ErrorBoundary />}
        />
      )
    );

Route 組件的參數説明:

interface RouteObject {
  path?: string;
  index?: boolean;
  children?: RouteObject[];
  caseSensitive?: boolean;
  id?: string;
  loader?: LoaderFunction;
  action?: ActionFunction;
  element?: React.ReactNode | null;
  hydrateFallbackElement?: React.ReactNode | null;
  errorElement?: React.ReactNode | null;
  Component?: React.ComponentType | null;
  HydrateFallback?: React.ComponentType | null;
  ErrorBoundary?: React.ComponentType | null;
  handle?: RouteObject["handle"];
  shouldRevalidate?: ShouldRevalidateFunction;
  lazy?: LazyRouteFunction<RouteObject>;
}
  • path:與 URL 匹配的路徑模式以確定此路由是否匹配 URL、鏈接 href 或表單操作。
  • index:通俗理解就是,被標記為 index 的路由就是默認子路由。

    <Route path="/teams" element={<Teams />}>
        <Route index element={<TeamsIndex />} />
        <Route path=":teamId" element={<Team />} />
    </Route>
  • children:用於嵌套路由的場景。
  • caseSensitive:是否區分大小寫。在組件中添加此屬性則為嚴格匹配:

    <Route caseSensitive path="/wEll-aCtuA11y" /> // 不匹配 "well-actua11y"
  • loader:路由渲染之前被調用的回調,並且通過 useLoaderData 給路由提供數據。

    <Route
      path="/teams/:teamId"
      loader={({ params }) => {
        return fetchTeam(params.teamId);
      }}
    />;
    
    function Team() {
      let team = useLoaderData();
      // ...
    }
  • action:當從 form、fetcher 或 submission 將提交發送到路由時,就調用路由操作。

    <Route
      path="/teams/:teamId"
      action={({ request }) => {
        const formData = await request.formData();
        return updateTeam(formData);
      }}
    />
  • element/Component
    如果要創建 React 元素,請使用element:

    <Route path="/for-sale" element={<Properties />} />

    否則使用ComponentReact Router 將為您創建 React Element:

    <Route path="/for-sale" Component={Properties} />
  • errorElement/ErrorBoundary
    當路由在 loaderaction 中渲染時拋出異常時,該 組件將代替正常組件進行渲染。

    <Route
        path="/for-sale"
        // if this throws an error while rendering
        element={<Properties />}
        // or this while loading properties
        loader={() => loadProperties()}
        // or this while creating a property
        action={async ({ request }) =>
          createProperty(await request.formData())
        }
        // then this element will render
        errorElement={<ErrorBoundary />}
    />
  • hydrateFallbackElement/HydrateFallback

    如果您正在使用服務器端渲染並且正在利用部分水合,那麼您可以在應用程序的初始水合期間指定要為非水合路線渲染的元素/組件。

  • handle

    任何特定於應用程序的數據。

  • lazy

    為了使您的應用程序包保持較小並且支持路由的代碼拆分,每個路由都可以提供一個異步函數來解析路由定義中與路由不匹配的部分(loaderactionComponent/elementErrorBoundary/errorElement等)。

    每個lazy函數通常會返回動態導入的結果:

    let routes = createRoutesFromElements(
      <Route path="/" element={<Layout />}>
        <Route path="a" lazy={() => import("./a")} />
        <Route path="b" lazy={() => import("./b")} />
      </Route>
    );

    如果是在惰性路由模塊中,導出想要為路由定義的屬性:

    export async function loader({ request }) {
      let data = await fetchData(request);
      return json(data);
    }
    
    export function Component() {
      let data = useLoaderData();
    
      return (
        <>
          <h1>You made it!</h1>
          <p>{data}</p>
        </>
      );
    }

    action

action 屬性允許你在路徑匹配時定義一個處理函數,用於處理表單提交等動作,並返回一個處理結果。

主要用途

  • 處理表單提交:當表單提交到某個路由時,action 可以處理提交的數據。
  • 處理用户交互:處理用户在特定路由上的交互,比如按鈕點擊等。
    每當應用向您的路線發送非獲取提交(postputpatchdelete)時,就會調用操作。這可能以幾種方式發生:
// forms
<Form method="post" action="/songs" />;
<fetcher.Form method="put" action="/songs/123/edit" />;

// imperative submissions
let submit = useSubmit();
submit(data, {
  method: "delete",
  action: "/songs/123",
});
fetcher.submit(data, {
  method: "patch",
  action: "/songs/123/edit",
});
  • params 解析動態路由的參數,比如下面示例的 projectId

    <Route
        path="/projects/:projectId/delete"
        action={({ params }) => {
            return fakeDeleteProject(params.projectId);
        }}
    />
  • request 從路由中獲取請求實例,最常見的用例是從請求中解析 FormData:

    <Route
        action={async ({ request }) => {
            let formData = await request.formData();
            // ...
        }}
    />

拋出錯誤:

<Route
  action={async ({ params, request }) => {
    const res = await fetch(
      `/api/properties/${params.id}`,
      {
        method: "put",
        body: await request.formData(),
      }
    );
    if (!res.ok) throw res;
    return { ok: true };
  }}
/>

根據不同的行為進行處理

async function action({ request }) {
  let formData = await request.formData();
  let intent = formData.get("intent");

  if (intent === "edit") {
    await editSong(formData);
    return { ok: true };
  }

  if (intent === "add") {
    await addSong(formData);
    return { ok: true };
  }

  throw json(
    { message: "Invalid intent" },
    { status: 400 }
  );
}

function Component() {
  let song = useLoaderData();

  // When the song exists, show an edit form
  if (song) {
    return (
      <Form method="post">
        <p>Edit song lyrics:</p>
        {/* Edit song inputs */}
        <button type="submit" name="intent" value="edit">
          Edit
        </button>
      </Form>
    );
  }

  // Otherwise show a form to add a new song
  return (
    <Form method="post">
      <p>Add new lyrics:</p>
      {/* Add song inputs */}
      <button type="submit" name="intent" value="add">
        Add
      </button>
    </Form>
  );
}

這段代碼展示瞭如何使用 action 屬性處理表單提交,並根據提交的“意圖”執行不同的操作(編輯或添加歌曲)。同時,組件根據數據的存在與否渲染相應的表單,提供了一個動態的用户界面。

以下是一個更詳細的示例,展示瞭如何使用 action 屬性處理表單提交,並在提交後進行重定向。

import React from 'react';
import { createBrowserRouter, RouterProvider, Route, Form, redirect } from 'react-router-dom';

// 定義 action 函數
const action = async ({ request }) => {
  const formData = await request.formData();
  const username = formData.get('username');
  // 處理提交的數據,比如保存到數據庫
  console.log('Username:', username);
  // 返回一個重定向
  return redirect('/success');
};

// 定義表單組件
function FormComponent() {
  return (
    <Form method="post">
      <label>
        Username:
        <input type="text" name="username" />
      </label>
      <button type="submit">Submit</button>
    </Form>
  );
}

// 定義成功頁面組件
function Success() {
  return <h2>Form submitted successfully!</h2>;
}

// 創建路由器
const router = createBrowserRouter([
  {
    path: '/',
    element: <FormComponent />,
    action: action,
  },
  {
    path: '/success',
    element: <Success />,  
  },
]);

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

export default App;

errorElement

在加載器(loaders)、動作(actions)或組件渲染過程中拋出異常時,路由(Routes)將不會走正常的渲染路徑(即 <Route element>),而是會走錯誤路徑(即 <Route errorElement>),並且可以通過 useRouteError 獲取到該錯誤。

<Route
  path="/invoices/:id"
  // if an exception is thrown here
  loader={loadInvoice}
  // here
  action={updateInvoice}
  // or here
  element={<Invoice />}
  // this will render instead of `element`
  errorElement={<ErrorBoundary />}
/>;

function Invoice() {
  return <div>Happy {path}</div>;
}

function ErrorBoundary() {
  let error = useRouteError();
  console.error(error);
  // Uncaught ReferenceError: path is not defined
  return <div>Dang!</div>;
}

冒泡

當路由沒有 時 errorElement,錯誤將通過父路由冒泡。在你的路由樹頂層放置一個 errorElement,你就可以在一個地方處理應用中的幾乎所有錯誤。或者你也可以在每個路由上都放置 errorElement

手動拋出異常

在errorElement處理意外錯誤的同時,它還可以用來處理預判到的異常。特別是在 loader 和action 中,執行結果是不受你控制的外部數據,數據可能不存在、服務補可用或用户無權訪問它。等等,都可以自定義異常並拋出。

<Route
  path="/properties/:id"
  element={<PropertyForSale />}
  errorElement={<PropertyError />}
  loader={async ({ params }) => {
    const res = await fetch(`/api/properties/${params.id}`);
    if (res.status === 404) {
      throw new Response("Not Found", { status: 404 });
    }
    const home = await res.json();
    const descriptionHtml = parseMarkdown(
      data.descriptionMarkdown
    );
    return { home, descriptionHtml };
  }}
/>

捕獲異常

所有拋出的自定義異常都會通過 useRouteError 獲取到,但如果你拋出一個 Response 的異常,React Router 會在返回給你的組件渲染之前自動解析響應數據。

然後,可以通過 isRouteErrorResponse 檢查這種特定類型的異常。結合 react-router-dom 包的 json,可以根據相關信息處理其邊界情況。

import { json } from "react-router-dom";

function loader() {
  const stillWorksHere = await userStillWorksHere();
  if (!stillWorksHere) {
    throw json(
      {
        sorry: "You have been fired.",
        hrEmail: "hr@bigco.com",
      },
      { status: 401 } // 狀態碼為 401
    );
  }
}

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

  // 如果是 response 錯誤且是 401 則渲染特殊的 DOM
  if (isRouteErrorResponse(error) && error.status === 401) {
    // the response json is automatically parsed to
    // `error.data`, you also have access to the status
    return (
      <div>
        <h1>{error.status}</h1>
        <h2>{error.data.sorry}</h2>
        <p>
          Go ahead and email {error.data.hrEmail} if you
          feel like this is a mistake.
        </p>
      </div>
    );
  }

  // rethrow to let the parent error boundary handle it
  // when it's not a special case for this route
  throw error;
}

有了 isRouteErrorResponse 後,則可以在根路由上創建一個通用的錯誤邊界處理其常見的錯誤問題:

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

  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return <div>This page doesn't exist!</div>;
    }

    if (error.status === 401) {
      return <div>You aren't authorized to see this</div>;
    }

    if (error.status === 503) {
      return <div>Looks like our API is down</div>;
    }

    if (error.status === 418) {
      return <div>🫖</div>;
    }
  }

  return <div>Something went wrong</div>;
}

hydrateFallbackElement

它允許你在服務器端渲染(SSR)時提供一個備用的 React 元素,當服務器端渲染的頁面在客户端進行水合(hydration)時,如果遇到某些組件或元素無法在服務器端渲染,就會使用這個備用元素。

let router = createBrowserRouter(
  [
    {
      id: "root",
      path: "/",
      loader: rootLoader,
      Component: Root,
      children: [
        {
          id: "invoice",
          path: "invoices/:id",
          loader: loadInvoice,
          Component: Invoice,
          HydrateFallback: InvoiceSkeleton,
        },
      ],
    },
  ],
  {
    future: {
      v7_partialHydration: true,
    },
    hydrationData: {
      root: {
        /*...*/
      },
      // No hydration data provided for the `invoice` route
    },
  }
);

組件

Await

await 組件是一個在 React Router v6 引入的新特性。它的作用是處理路由加載時的異步邏輯,比如數據獲取或懶加載組件。

import {
  defer,
  Route,
  useLoaderData,
  Await,
} from "react-router-dom";

// given this route
<Route
  loader={async () => {
    let book = await getBook();
    let reviews = getReviews(); // not awaited
    return defer({
      book,
      reviews, // this is a promise
    });
  }}
  element={<Book />}
/>;

function Book() {
  const {
    book,
    reviews, // this is the same promise
  } = useLoaderData();
  return (
    <div>
      <h1>{book.title}</h1>
      <p>{book.description}</p>
      <React.Suspense fallback={<ReviewsSkeleton />}>
        <Await
          // and is the promise we pass to Await
          resolve={reviews}
        >
          <Reviews />
        </Await>
      </React.Suspense>
    </div>
  );
}

Form

Form 組件是純 HTML表單的包裝器,用於模擬瀏覽器以進行客户端路由和數據變更。

import { Form } from "react-router-dom";

function NewEvent() {
  return (
    <Form method="post" action="/events">
      <input type="text" name="title" />
      <input type="text" name="description" />
      <button type="submit">Create</button>
    </Form>
  );
}
  • action 表單提交到的 URL,與HTML 表單操作類似。唯一的區別在於默認操作。對於 HTML 表單,默認為完整 URL。對於 <Form>,默認為上下文中最接近的路由的相對 URL。

    function ProjectsLayout() {
      return (
        <>
          <Form method="post" />
          <Outlet />
        </>
      );
    }
    
    function ProjectsPage() {
      return <Form method="post" />;
    }
    
    <DataBrowserRouter>
      <Route
        path="/projects"
        element={<ProjectsLayout />}
        action={ProjectsLayout.action}
      >
        <Route
          path=":projectId"
          element={<ProjectsPage />}
          action={ProjectsPage.action}
        />
      </Route>
    </DataBrowserRouter>;
  • method 執行請求的方法,除了 getpost 外,還支持 putpatchdelete;默認 get
  • navigate:可以指定 <Form navigate={false}> ,讓表單跳過導航,在內部使用 fetcher。這基本上是 useFetcher() + <fetcher.Form> 的簡寫。
  • fetcherKey:在使用非導航 Form 時,也可選擇通過 <Form navigate={false} fetcherKey="my-key"> 指定自己的取值器密鑰。
  • replace:替換當前路由

    <Form replace />
  • relative:默認情況下,路徑是相對於路由層次結構而言的,因此 .. 會向上移動一級 Route。有時,你可能會發現有一些匹配的 URL 模式沒有嵌套的意義,這時你更願意使用相對路徑路由。

    <Form to="../some/where" relative="path">
  • reloadDocument:指示表單跳過 React Router,使用瀏覽器內置行為提交表單。

    這個方法一般用於 remix 框架,否則還是乖乖的使用原生的 form 標籤吧!
    <Form reloadDocument />

    建議使用 <form> ,這樣可以獲得默認和相對 action 的好處,除此之外,它與普通 HTML 表單相同。

  • state:可用於為存儲在歷史狀態中的新位置設置一個有狀態的值。

    <Form
        method="post"
        action="new-path"
        state={{ some: "value" }}
    />

    可以在 "新路徑 "路由上訪問該狀態值:

    const { state } = useLocation();
  • preventScrollReset:如果使用 <ScrollRestoration>,則可以防止在表單操作重定向到新位置時,滾動位置被重置到窗口頂部。

    <Form method="post" preventScrollReset={true} />
  • unstable_viewTransition:該屬性通過在 document.startViewTransition() 中封裝最終狀態更新,為該導航啓用了視圖轉換。如果需要為該視圖轉換應用特定樣式,還需要利用 unstable_useViewTransitionState()

    該屬性是一個實驗性的 API,因為原生的 startViewTransition 是一個實驗性的 API。

Link

這個組件分為 RN 版本的 WEB 版本,我們這系列着看 WEB 版本。

Link 就是一個能跳轉的 <a> 標籤,用户可以通過點擊或輕點它來導航到另一個頁面。在 react-router-dom 中, <Link> 會渲染一個可訪問的 <a> 元素,該元素帶有一個真正的 href,指向它所鏈接的資源。這

declare function Link(props: LinkProps): React.ReactElement;

interface LinkProps
  extends Omit<
    React.AnchorHTMLAttributes<HTMLAnchorElement>,
    "href"
  > {
  to: To;
  preventScrollReset?: boolean;
  relative?: "route" | "path";
  reloadDocument?: boolean;
  replace?: boolean;
  state?: any;
  unstable_viewTransition?: boolean;
}

type To = string | Partial<Path>;

interface Path {
  pathname: string;
  search: string;
  hash: string;
}

簡單示例如下:

import * as React from "react";
import { Link } from "react-router-dom";

function UsersIndexPage({ users }) {
  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <Link to={user.id}>{user.name}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

相對 <Link to> 值(不以 / 開頭)是相對於父路由解析的,這意味着它建立在渲染該 <Link> 的路由所匹配的 URL 路徑之上。它可能包含 .. ,以鏈接到層級更高的路由。在這種情況下, .. 的工作原理與命令行函數 cd 完全相同;每 .. 刪除父路徑中的一段。

噹噹前 URL 以 / 結尾時,帶有 ..<Link to> 與普通 <a href> 的行為不同。 <Link to> 會忽略尾部斜線,併為每個 .. 刪除一個 URL 段。但是,噹噹前 URL 以 / 結尾時, <a href> 值處理 .. 的方式與其不同。

此組件的的參數跟 Form 中的參數差不多,這裏就不再介紹了。

NavLink

<NavLink> 是一種特殊的 <Link> ,它知道自己是否處於 "激活"、"待定 "或 "過渡 "狀態。這在幾種不同的情況下都很有用:

  • 在創建導航菜單(如麪包屑或一組選項卡)時,您希望顯示當前選擇了哪個選項卡
  • 它為屏幕閲讀器等輔助技術提供了有用的背景信息
  • 它提供了一個 "過渡 "值,可讓您對視圖轉換進行更精細的控制
import { NavLink } from "react-router-dom";

<NavLink
  to="/messages"
  className={({ isActive, isPending }) =>
    isPending ? "pending" : isActive ? "active" : ""
  }
>
  Messages
</NavLink>;

默認 active 類

默認情況下,當組件處於活動狀態時,active會向其添加一個類,以便您可以使用 CSS 來設置其樣式。

<nav id="sidebar">
  <NavLink to="/messages" />
</nav>
#sidebar a.active {
    color: red;
}

className

className prop 的工作方式與普通的 className 類似,也可以向其傳遞一個函數,以根據鏈接的活動狀態和待處理狀態自定義所應用的 className

<NavLink
  to="/messages"
  className={({ isActive, isPending, isTransitioning }) =>
    [
      isPending ? "pending" : "",
      isActive ? "active" : "",
      isTransitioning ? "transitioning" : "",
    ].join(" ")
  }
>
  Messages
</NavLink>

style

style 屬性的工作方式與普通樣式屬性類似,也可以通過一個函數,根據鏈接的活動和待定狀態自定義應用的樣式。

<NavLink
  to="/messages"
  style={({ isActive, isPending, isTransitioning }) => {
    return {
      fontWeight: isActive ? "bold" : "",
      color: isPending ? "red" : "black",
      viewTransitionName: isTransitioning ? "slide" : "",
    };
  }}
>
  Messages
</NavLink>

children

可以傳遞一個呈現屬性作為子元素,以便根據活動和待定狀態自定義 <NavLink> 的內容,這對更改內部元素的樣式非常有用。

<NavLink to="/tasks">
  {({ isActive, isPending }) => (
    <span className={isActive ? "active" : ""}>Tasks</span>
  )}
</NavLink>

end

end 屬性更改了 activepending 狀態的匹配邏輯,使其只匹配到導航鏈接 to 路徑的 "末端"。如果 URL 長於 to ,將不再被視為激活狀態。

Link Current URL isActive
<NavLink to="/tasks" /> / tasks true
<NavLink to="/tasks" /> /tasks/123 true
<NavLink to="/tasks" end /> /tasks true
<NavLink to="/tasks" end /> /tasks/123 false
<NavLink to="/tasks/" end /> /tasks false
<NavLink to="/tasks/" end /> /tasks/ true

關於根路由鏈接的説明

<NavLink to="/"> 是一個特例,因為每個 URL 都匹配 / 。為了避免默認情況下每條路由都匹配,它實際上忽略了 end 屬性,只在根路由上匹配。

caseSensitive

添加 caseSensitive 屬性後,匹配邏輯會發生變化,變得區分大小寫。

Link URL isActive
<NavLink to="/SpOnGe-bOB" /> /sponge-bob true
<NavLink to="/SpOnGe-bOB" caseSensitive /> /sponge-bob false

reloadDocument 此屬性可用於跳過客户端路由,讓瀏覽器正常處理轉換(如同 <a href> )。

unstable_viewTransition 用以動畫過渡場景下的,現在還是一個實驗特性。謹慎使用!

Navigate

<Navigate> 元素在渲染時會改變當前位置。它是 useNavigate 的組件包裝器,並接受與 props 相同的參數。

declare function Navigate(props: NavigateProps): null;

interface NavigateProps {
  to: To;
  replace?: boolean;
  state?: any;
  relative?: RelativeRoutingType;
}

示例如下:

import * as React from "react";
import { Navigate } from "react-router-dom";

class LoginForm extends React.Component {
  state = { user: null, error: null };

  async handleSubmit(event) {
    event.preventDefault();
    try {
      let user = await login(event.target);
      this.setState({ user });
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    let { user, error } = this.state;
    return (
      <div>
        {error && <p>{error.message}</p>}
        {user && (
          <Navigate to="/dashboard" replace={true} />
        )}
        <form
          onSubmit={(event) => this.handleSubmit(event)}
        >
          <input type="text" name="username" />
          <input type="password" name="password" />
        </form>
      </div>
    );
  }

Outlet

父路由元素中應使用 <Outlet> 來呈現其子路由元素。這樣就可以在呈現子路由時顯示嵌套用户界面。如果父路由完全匹配,則會呈現子索引路由;如果沒有索引路由,則不會呈現任何內容。

interface OutletProps {
  context?: unknown;
}
declare function Outlet(
  props: OutletProps
): React.ReactElement | null; 

示例如下:

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* This element will render either <DashboardMessages> when the URL is
          "/messages", <DashboardTasks> at "/tasks", or null if it is "/"
      */}
      <Outlet />
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />}>
        <Route
          path="messages"
          element={<DashboardMessages />}
        />
        <Route path="tasks" element={<DashboardTasks />} />
      </Route>
    </Routes>
  );
}

定義一個簡單的路由結構,其中包含一個 Dashboard 組件和一些子路由。這裏的 Dashboard 組件作為父路由,DashboardMessages 和 DashboardTasks 作為子路由。

Dashboard 是一個簡單的 React 組件,包含一個標題 "Dashboard" 和一個 <Outlet /> 組件。

<Outlet /> 是 React Router 提供的組件,用於在父路由中渲染匹配的子路由組件。當 URL 匹配子路由的路徑時,子路由組件會被渲染到 <Outlet /> 位置。這樣就實現了在不同路徑下渲染不同的組件。

Routes 與 Route

在應用程序中的任何地方,<Routes> 都會匹配當前位置的一組子路由。

interface RoutesProps {
    children?: React.ReactNode;
    location?: Partial<Location> | string;
}

<Routes location>
    <Route />
</Routes>;
如果使用的是 createBrowserRouter 這樣的數據路由器創建路由,使用 Routes 組件的情況並不常見,因為作為 <Routes> 樹的後代的一部分定義的路由無法利用 RouterProvider 應用程序可用的數據 API。

每當位置發生變化時, <Routes> 就會查看其所有子路由,找出最匹配的路由,並渲染用户界面的該分支。 <Route> 元素可以嵌套,以表示嵌套的用户界面,這也與嵌套的 URL 路徑相對應。父路由通過呈現 <Outlet>來呈現其子路由。

<Routes>
<Route path="/" element={<Dashboard />}>
  <Route
    path="messages"
    element={<DashboardMessages />}
  />
  <Route path="tasks" element={<DashboardTasks />} />
</Route>
<Route path="about" element={<AboutPage />} />
</Routes>

ScrollRestoration

將在加載程序完成後,模擬瀏覽器在位置更改時的滾動恢復功能,以確保滾動位置恢復到正確位置,甚至跨域滾動。

只需呈現其中一個,建議在應用程序的根路由中呈現:

import { ScrollRestoration } from "react-router-dom";

function RootRouteComponent() {
  return (
    <div>
      {/* ... */}
      <ScrollRestoration />
    </div>
  );
}
  • getKey:可選屬性,用於定義 React Router 恢復滾動位置時應使用的鍵。

    <ScrollRestoration
      getKey={(location, matches) => {
        // default behavior
        return location.key;
      }}
    />

    默認情況下,它使用 location.key ,在沒有客户端路由的情況下模擬瀏覽器的默認行為。用户可以在堆棧中多次導航到相同的 URL,每個條目都有自己的滾動位置來還原。

    有些應用可能希望覆蓋這一行為,並根據其他內容恢復位置。例如,一個社交應用程序有四個主要頁面:

    • "/home"
    • "/messages"
    • "/notifications"
    • "/search"

    如果用户從 "/home" 開始,向下滾動一點,點擊導航菜單中的 "信息",然後點擊導航菜單中的 "主頁"(而不是返回按鈕!),歷史堆棧中就會出現三個條目:

    1. /home
    2. /messages
    3. /home

    默認情況下,React Router(和瀏覽器)會為 1 和 3 存儲兩個不同的滾動位置,即使它們的 URL 相同。這意味着當用户從 2 → 3 瀏覽時,滾動位置會移到頂部,而不是恢復到 1 中的位置。

    這裏一個可靠的產品決策是,無論用户如何到達(返回按鈕或新鏈接點擊),都要保持他們在主頁上的滾動位置。為此,您需要使用 location.pathname 作為關鍵字。

    <ScrollRestoration
        getKey={(location, matches) => {
            return location.pathname;
        }}
    />

    或者,您可能只想對某些路徑使用路徑名,而對其他路徑使用正常行為:

    <ScrollRestoration
      getKey={(location, matches) => {
        const paths = ["/home", "/notifications"];
        return paths.includes(location.pathname)
          ? // home and notifications restore by pathname
            location.pathname
          : // everything else by location like the browser
            location.key;
      }}
    />
  • 防止滾動重置

    當導航創建新的滾動鍵時,滾動位置會重置為頁面頂部。您可以防止鏈接和表單出現 "滾動到頂部 "行為:

    <Link preventScrollReset={true} />
    <Form preventScrollReset={true} />
  • 滾動閃爍

    如果沒有 Remix 這樣的服務器端渲染框架,在初始頁面加載時可能會出現一些滾動閃爍。這是因為 React Router 無法還原滾動位置,直到您的 JS 捆綁包下載完畢、數據加載完畢、整個頁面渲染完畢(如果您正在渲染一個旋轉器,視口很可能不是保存滾動位置時的大小)。

    服務器渲染框架可以防止滾動閃爍,因為它們可以在首次加載時發送一個完整的文檔,因此可以在頁面首次渲染時恢復滾動。

Hooks

官方提供了很多 Hook,其實上面我們也用到了不少,下面就不一一演示了:

  • useActionData:提供上一次導航 action 結果的返回值,如果沒有提交,則提供 undefined 。這個 Hook 最常用的情況是表單驗證錯誤。
  • useAsyncError:從最近的 <Await> 組件返回拒絕值。
  • useAsyncValue:從最近的 <Await> 父組件返回已解析的數據。
  • useBeforeUnload:該鈎子只是 window.onbeforeunload 的一個輔助工具。在用户離開頁面之前,將重要的應用程序狀態保存在頁面上(如瀏覽器的本地存儲)可能會很有用。這樣,如果用户回來,就可以恢復任何狀態信息(恢復表單輸入值等)。
  • useBlocker:通過 useBlocker 鈎子,可以阻止用户從當前位置導航,併為他們提供自定義用户界面,讓他們確認導航。
  • useFetcher:請求數據用的
  • useFetchers:返回所有不帶 load 、 submit 或 Form 屬性正在進行的 fetchers 數組,但不包括它們的 load , submit 或 Form 屬性(不能讓父組件試圖控制其子組件的行為!根據實際經驗,我們知道這是很愚蠢的做法)。
  • useFormAction:自動根據上下文解析當前路由的默認操作和相對操作。
  • useHref:返回一個 URL,可用於鏈接到給定的 to 位置,即使在 React Router 之外也是如此。
  • useInRouterContext:如果組件是在 <Router> 的上下文中呈現,則 useInRouterContext 鈎子返回 true ,否則返回 false 。這對某些需要知道自己是否在 React Router 應用程序上下文中呈現的第三方擴展很有用。
  • useLinkClickHandler:返回一個用於導航的點擊事件處理程序。
  • useLinkPressHandler:返回一個用於自定義 <Link> 導航的按壓事件處理程序。
  • useLoaderData:提供路由 loader 返回的值。
  • useLocation:返回當前 location 對象。
  • useMatch:返回給定路徑上的路由相對於當前位置的匹配數據。
  • useMatches:返回頁面上匹配的當前路由。
  • useNavigate:會返回一個函數,讓你以編程方式導航。
  • useNavigation:該鈎子會告訴你關於頁面導航的一切信息,以便在數據突變時建立待定的導航指示器和優化的用户界面。例如:

    • 全局加載指示器
    • 在發生突變時禁用表單
    • 在提交按鈕上添加繁忙指示器
    • 在服務器上創建新記錄時優化的顯示新記錄
    • 在更新記錄時優化的顯示記錄的新狀態
  • useNavigationType:返回當前的導航類型或用户是如何進入當前頁面的;可以是通過歷史堆棧上的彈出、推送或替換操作。
  • useOutlet:返回子路由在該路由層次結構中的元素。<Outlet> 內部使用此鈎子來呈現子路由。
  • useOutletContext:父路由通常會管理狀態或其他你希望與子路由共享的值。
  • useParams:鈎子會返回一個由 <Route path> 匹配的當前 URL 動態參數的鍵/值對組成的對象。
  • unstable_usePrompt:鈎子允許您在導航離開當前位置前通過 window.confirm 提示用户進行確認。
  • useResolvedPath:此鈎子根據當前位置的路徑名解析給定 to 值中位置的 pathname
  • useRevalidator:此鈎子允許您以任何理由重新驗證數據。React Router 會在調用操作後自動重新驗證數據,但您也可能出於其他原因(如焦點返回窗口時)需要重新驗證數據。
  • useRouteError:在 errorElement 中,該鈎子會返回在操作、加載器或渲染過程中拋出的任何響應。
  • useRouteLoaderData:這個鈎子可以讓當前呈現的路由數據在樹中的任何位置都可用。這對於樹中較深位置的組件需要更遠位置路由的數據,以及父路由需要樹中較深位置子路由的數據時非常有用。
  • useRoutes:鈎子的功能上等同於 <Routes>,但它使用 JavaScript 對象而不是 <Route> 元素 元素來定義路由。useRoutes 的返回值要麼是一個有效的 React 元素,可以用來呈現路由樹;要麼是 null (如果沒有匹配的元素)。
  • useSearchParams:用於讀取和修改當前位置 URL 中的查詢字符串。
  • useSubmit<Form> 的命令式版本,讓程序員代替用户提交表單。
  • unstable_useViewTransitionState: 當指定位置有活動視圖轉換時,此 Hook 會返回 true 。這可用於對元素應用更精細的樣式,以進一步自定義視圖轉換。這要求通過 Link (或 Form, navigatesubmit 調用)上的 unstable_viewTransition 中啓用指定導航的視圖轉換。

請求方法

  • json
    快捷方式:

    new Response(JSON.stringify(someValue), {
        headers: {
            "Content-Type": "application/json; utf-8",
        },
    });

    通常用於 loader

    import { json } from "react-router-dom";
    
    const loader = async () => {
      const data = getSomeData();
      return json(data);
    };
  • redirect

    由於可以在 loadersactions 中返回或拋出響應,因此可以使用 redirect 重定向到另一個路由。

  • redirectDocument

    這是 redirect 的一個小封裝,它將觸發文檔級重定向到新位置,而不是客户端導航。

  • replace

    這是一個圍繞重定向的封裝,它將使用 history.replaceState 代替 history.pushState 觸發客户端重定向到新位置。

實用工具

  • createRoutesFromChildren:其實它是 createRoutesFromElements 的別名。
  • createRoutesFromElements:是一個從 <Route> 元素創建路由對象的輔助工具。
  • createSearchParams:是對 new URLSearchParams(init) 的輕量級封裝,增加了對使用具有數組值的對象的支持。該函數與 useSearchParams 內部使用的從 URLSearchParamsInit 值創建 URLSearchParams 對象的函數相同。
  • defer:該實用程序允許您通過傳遞承諾而不是解析值來延遲從loader返回的值。
  • generatePath:將一組參數插值為路由路徑字符串,其中包含 :id* 佔位符。
  • isRouteErrorResponse:如果路由錯誤是路由錯誤響應,則返回 true
  • Location:React Router 中的 "位置 "一詞指的是 history 庫中的 Location 接口。
  • matchPath:將路由路徑模式與 URL 路徑名進行匹配,並返回匹配信息。
  • matchRoutes:針對一組路由與給定的 location 運行路由匹配算法,查看哪些路由(如果有)匹配。如果發現匹配,就會返回一個 RouteMatch 對象數組,每個匹配路由對應一個對象。
  • renderMatchesrenderMatches 會將 matchRoutes() 的結果渲染為一個 React 元素。
  • resolvePathresolvePath 將給定的 To 值解析為具有絕對 pathname 的實際 Path 對象。每當您需要知道相對 To 值的確切路徑時,這個功能就非常有用。例如, `<Link> 使用該函數來了解其指向的實際 URL。

總結

React Router 是單頁應用(SPA)中管理 URL 和視圖映射關係的重要工具。它在不同版本中不斷演進,提供了更強大、靈活的路由管理功能。當前主流版本的 React Router 具備了多個核心組件和功能,使得路由配置更加簡單和直觀。

  1. 核心組件與功能
  2. BrowserRouterHashRouter 是兩個基礎的路由器,分別適用於不同的環境和需求。
  3. RoutesRoute 是定義路由和嵌套路由的基本單元,結合 LinkNavLink,可以輕鬆創建導航和管理路由的激活狀態。
  4. useNavigateuseLocation 作為鈎子,提供了編程式導航和獲取當前路由信息的能力。
  5. Outlet 用於處理嵌套路由的內容渲染,是實現多級路由結構的關鍵。
  6. 高級用法
  7. 動態路由 可以通過 URL 參數和查詢參數實現個性化頁面展示。
  8. 使用 懶加載 可以提高應用性能,結合 lazySuspense 組件,能夠在用户訪問特定頁面時按需加載代碼。
  9. Navigate 組件用於頁面重定向,保證用户在特定條件下訪問正確的頁面。
  10. 路由守衞 可以通過自定義鈎子或組件實現,確保用户訪問受保護的路由時經過驗證。
  11. 狀態管理的結合

React Router 與 Redux 或 Context API 等狀態管理工具的結合,確保應用在路由切換時保持狀態的一致性。處理好路由切換時的組件狀態問題,有助於提升用户體驗。

  1. 常見問題
    在開發中,可能會遇到路徑基準(basename)設置、組件不更新以及 404 頁面處理等問題。通過正確的配置和使用,React Router 能有效避免這些常見坑。
  2. 性能優化
    為了進一步提升應用的性能,React Router 提供了諸如組件懶加載、緩存策略等優化手段,減少不必要的重新渲染,提升頁面響應速度。
user avatar yaofly 頭像 susouth 頭像 mulander 頭像 iymxpc3k 頭像 light_5cfbb652e97ce 頭像 codeoop 頭像
6 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.