主體
分支
主體 (6.23.1)dev
版本
6.23.1v4/5.xv3.x
功能概述
在此頁面

功能概觀

用戶端導向

React Router 啟用了「用戶端導向」。

在傳統網站中,瀏覽器會從網頁伺服器要求文件,下載並評估 CSS 和 JavaScript 資源,並渲染伺服器送出的 HTML。當使用者按下連結時,整個流程會重新執行一次以載入新頁面。

用戶端導向讓你的應用程式可以藉由連結點擊更新網址,而不用再向伺服器要求另一個文件。相反地,你的應用程式可以立即渲染某些新的使用者介面,並透過 fetch 建立資料要求以使用新的資訊更新頁面。

這可提供更快的使用者體驗,因為瀏覽器不需要要求載入全新的文件,也不需要重新評估下個頁面的 CSS 和 JavaScript 資源。它也讓使用者體驗更動態,例如加入動畫。

透過建立 Router 並使用 Link<Form> 進行連結或提交,可以啟用用戶端導向。

import * as React from "react";
import { createRoot } from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
  Route,
  Link,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about">About Us</Link>
      </div>
    ),
  },
  {
    path: "about",
    element: <div>About</div>,
  },
]);

createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

巢狀路由

巢狀路由是指將 URL 片段與元件階層及資料連結起來的一般概念。React 路由的巢狀路由概念源自於 2014 年左右的 Ember.js 路由系統。Ember 團隊發現,在幾乎每一個案例中,URL 的片段都能決定

  • 頁面要呈現的版面
  • 這些版面的資料依賴項

React 路由採用這個慣例,並提供 API 來建立與 URL 片段和資料結合的巢狀版面。

// Configure nested routes with JSX
createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Root />}>
      <Route path="contact" element={<Contact />} />
      <Route
        path="dashboard"
        element={<Dashboard />}
        loader={({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          })
        }
      />
      <Route element={<AuthLayout />}>
        <Route
          path="login"
          element={<Login />}
          loader={redirectIfUser}
        />
        <Route path="logout" action={logoutUser} />
      </Route>
    </Route>
  )
);

// Or use plain objects
createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "contact",
        element: <Contact />,
      },
      {
        path: "dashboard",
        element: <Dashboard />,
        loader: ({ request }) =>
          fetch("/api/dashboard.json", {
            signal: request.signal,
          }),
      },
      {
        element: <AuthLayout />,
        children: [
          {
            path: "login",
            element: <Login />,
            loader: redirectIfUser,
          },
          {
            path: "logout",
            action: logoutUser,
          },
        ],
      },
    ],
  },
]);

這個視覺化可能會有所幫助。

動態片段

URL 片段可以是動態佔位符,這些佔位符會經過解析並提供給各種 API。

<Route path="projects/:projectId/tasks/:taskId" />

帶有 : 的兩個片段是動態的,並提供給以下 API

// If the current location is /projects/abc/tasks/3
<Route
  // sent to loaders
  loader={({ params }) => {
    params.projectId; // abc
    params.taskId; // 3
  }}
  // and actions
  action={({ params }) => {
    params.projectId; // abc
    params.taskId; // 3
  }}
  element={<Task />}
/>;

function Task() {
  // returned from `useParams`
  const params = useParams();
  params.projectId; // abc
  params.taskId; // 3
}

function Random() {
  const match = useMatch(
    "/projects/:projectId/tasks/:taskId"
  );
  match.params.projectId; // abc
  match.params.taskId; // 3
}

請參閱

排名路線比對

在將 URL 與路線進行比對時,React 路由會根據片段數、靜態片段、動態片段、雜湊等來對路線進行排名,並選擇最明確 的比對結果。

例如,考慮以下兩個路線

<Route path="/teams/:teamId" />
<Route path="/teams/new" />

現在假設 URL 是 http://example.com/teams/new

儘管兩個路線在技術上都符合 URL(new 可能為 :teamId),但直觀上我們知道我們希望選擇第二個路線(/teams/new)。React 路由的比對演算法也知道這一點。

透過排名路線,您不必擔心路線的順序。

大多數的網路應用程式都在使用者介面的上方、側邊欄中,而且經常是多個層級,設有長期的導覽區段。使用 <NavLink> 可以輕鬆為主動導覽項目套用樣式,讓使用者知道自己在應用程式內的所在位置(isActive),或知道他們將會前往何處(isPending)。

<NavLink
  style={({ isActive, isPending }) => {
    return {
      color: isActive ? "red" : "inherit",
    };
  }}
  className={({ isActive, isPending }) => {
    return isActive ? "active" : isPending ? "pending" : "";
  }}
/>

您也可以將useMatch用於連結以外的任何其他「主動」指示。

function SomeComp() {
  const match = useMatch("/messages");
  return <li className={Boolean(match) ? "active" : ""} />;
}

請參閱

類似於 HTML <a href><Link to><NavLink to> 可以使用相對路徑,並在處理巢狀路由時有增強行為。

假設有以下路線設定

<Route path="home" element={<Home />}>
  <Route path="project/:projectId" element={<Project />}>
    <Route path=":taskId" element={<Task />} />
  </Route>
</Route>

考慮以下 URL https://example.com/home/project/123,它會呈現以下路線元件階層

<Home>
  <Project />
</Home>

如果 <Project /> 呈現以下連結,連結的 href 會像這樣解析

在 @ /home/project/123<Project> 解析的 <a href>
<Link to="abc"> /home/project/123/abc
<Link to="."> /home/project/123
<Link to=".."> /home
<Link to=".." relative="path"> /home/project

請注意,第一個..會移除project/:projectId路由中的兩個片段。預設情況下,相對連結中的..會遍歷路由階層,而不是 URL 片段。在下一範例中加入relative="path",這樣就允許您遍歷路徑片段。

相對連結永遠相對於其所渲染的路由路徑,而不是完整的 URL。這表示如果使用者使用<Link to="abc">導覽到<Task />的 URL /home/project/123/abc,則<Project>中的 href 並不會改變(這與一般的<a href>相反,而這是客戶端路由中的常見問題)。

資料載入

因為 URL 片段通常會對映到您應用程式的持續資料,所以 React Router 提供了傳統的資料載入掛勾,以在導覽期間啟動資料載入。結合巢狀路由,可以並行載入特定 URL 中所有多重版面的資料。

<Route
  path="/"
  loader={async ({ request }) => {
    // loaders can be async functions
    const res = await fetch("/api/user.json", {
      signal: request.signal,
    });
    const user = await res.json();
    return user;
  }}
  element={<Root />}
>
  <Route
    path=":teamId"
    // loaders understand Fetch Responses and will automatically
    // unwrap the res.json(), so you can simply return a fetch
    loader={({ params }) => {
      return fetch(`/api/teams/${params.teamId}`);
    }}
    element={<Team />}
  >
    <Route
      path=":gameId"
      loader={({ params }) => {
        // of course you can use any data store
        return fakeSdk.getTeam(params.gameId);
      }}
      element={<Game />}
    />
  </Route>
</Route>

可以使用 useLoaderData 向您的元件提供資料。

function Root() {
  const user = useLoaderData();
  // data from <Route path="/">
}

function Team() {
  const team = useLoaderData();
  // data from <Route path=":teamId">
}

function Game() {
  const game = useLoaderData();
  // data from <Route path=":gameId">
}

當使用者造訪或點選連結到 https://example.com/real-salt-lake/45face3 時,所有三個路由載入器都將在平行載入並呼叫,而後才會渲染該 URL 的 UI。

重新導向

載入或變更資料時,通常會將使用者重新導向到另一個路由。

<Route
  path="dashboard"
  loader={async () => {
    const user = await fake.getUser();
    if (!user) {
      // if you know you can't render the route, you can
      // throw a redirect to stop executing code here,
      // sending the user to a new route
      throw redirect("/login");
    }

    // otherwise continue
    const stats = await fake.getDashboardStats();
    return { user, stats };
  }}
/>
<Route
  path="project/new"
  action={async ({ request }) => {
    const data = await request.formData();
    const newProject = await createProject(data);
    // it's common to redirect after actions complete,
    // sending the user to the new record
    return redirect(`/projects/${newProject.id}`);
  }}
/>

請參閱

導覽 UI 待處理

當使用者在應用程式中導覽時,下一個頁面的資料會在頁面渲染前載入。這時提供使用者回饋資料很重要,否則應用程式會感覺沒有回應。

function Root() {
  const navigation = useNavigation();
  return (
    <div>
      {navigation.state === "loading" && <GlobalSpinner />}
      <FakeSidebar />
      <Outlet />
      <FakeFooter />
    </div>
  );
}

請參閱

使用 <Suspense> 的骨架 UI

與其等待下一個頁面的資料,您可以defer資料,如此一來當資料載入時,UI 將立即切換到下一個畫面,其中包含暫位元件。

<Route
  path="issue/:issueId"
  element={<Issue />}
  loader={async ({ params }) => {
    // these are promises, but *not* awaited
    const comments = fake.getIssueComments(params.issueId);
    const history = fake.getIssueHistory(params.issueId);
    // the issue, however, *is* awaited
    const issue = await fake.getIssue(params.issueId);

    // defer enables suspense for the un-awaited promises
    return defer({ issue, comments, history });
  }}
/>;

function Issue() {
  const { issue, history, comments } = useLoaderData();
  return (
    <div>
      <IssueDescription issue={issue} />

      {/* Suspense provides the placeholder fallback */}
      <Suspense fallback={<IssueHistorySkeleton />}>
        {/* Await manages the deferred data (promise) */}
        <Await resolve={history}>
          {/* this calls back when the data is resolved */}
          {(resolvedHistory) => (
            <IssueHistory history={resolvedHistory} />
          )}
        </Await>
      </Suspense>

      <Suspense fallback={<IssueCommentsSkeleton />}>
        <Await resolve={comments}>
          {/* ... or you can use hooks to access the data */}
          <IssueComments />
        </Await>
      </Suspense>
    </div>
  );
}

function IssueComments() {
  const comments = useAsyncValue();
  return <div>{/* ... */}</div>;
}

請參閱

資料突變

HTML 表單是導覽事件,就像連結一樣。React Router 支援使用客戶端路由的 HTML 表單工作流程。

當提交表單時,將會防止正常的瀏覽器導覽事件,並建立一個Request,其中包含包含提交的 FormData 的本文。此請求會傳送給與表單的 <Form action> 相符的 <Route action>

表單元素的 name 屬性會提交至動作

<Form action="/project/new">
  <label>
    Project title
    <br />
    <input type="text" name="title" />
  </label>

  <label>
    Target Finish Date
    <br />
    <input type="date" name="due" />
  </label>
</Form>

正常的 HTML 文件請求會被阻止,並且寄送至比對到的路由動作 (與 <form action> 相符的 <Route path>),包括 request.formData

<Route
  path="project/new"
  action={async ({ request }) => {
    const formData = await request.formData();
    const newProject = await createProject({
      title: formData.get("title"),
      due: formData.get("due"),
    });
    return redirect(`/projects/${newProject.id}`);
  }}
/>

資料重新驗證

數十年前的 Web 慣例指出,當表單張貼到伺服器時,資料正在變更,而且必須重新產生一個新頁面。此慣例已在 React Router 基於 HTML 的資料變更 API 中遵循實施。

在呼叫路由動作之後,頁面中所有資料的載入程式會再次呼叫,以確保 UI 會自動地與資料保持最新狀態。不需要讓快取金鑰過期,也不需要重新載入內容提供者。

請參閱

忙碌指示器

當表單正提交至路由動作時,你可以存取導航狀態,以顯示忙碌指示器、停用欄位集等等。

function NewProjectForm() {
  const navigation = useNavigation();
  const busy = navigation.state === "submitting";
  return (
    <Form action="/project/new">
      <fieldset disabled={busy}>
        <label>
          Project title
          <br />
          <input type="text" name="title" />
        </label>

        <label>
          Target Finish Date
          <br />
          <input type="date" name="due" />
        </label>
      </fieldset>
      <button type="submit" disabled={busy}>
        {busy ? "Creating..." : "Create"}
      </button>
    </Form>
  );
}

請參閱

樂觀 UI

了解即將傳送至動作formData 通常就足夠用來略過忙碌指示器,並立即在下一狀態中呈現 UI,即使非同步作業仍尚未完成。這稱為「樂觀 UI」。

function LikeButton({ tweet }) {
  const fetcher = useFetcher();

  // if there is `formData` then it is posting to the action
  const liked = fetcher.formData
    ? // check the formData to be optimistic
      fetcher.formData.get("liked") === "yes"
    : // if its not posting to the action, use the record's value
      tweet.liked;

  return (
    <fetcher.Form method="post" action="toggle-liked">
      <button
        type="submit"
        name="liked"
        value={liked ? "yes" : "no"}
      />
    </fetcher.Form>
  );
}

(沒錯,HTML 按鈕可以具有 namevalue)。

雖然使用 fetcher 來進行樂觀 UI 會比較常見,但你也可以使用 navigation.formData 來用一般的表單進行相同的作業。

資料擷取程式

HTML 表單是變更的模型,但是它們有一個嚴重的限制:一次只能有一個表單,因為提交表單等於導航。

大部分的 Web 應用程式都需要同時進行多個變更,例如一串記載表格,其中的每一條記載都能獨立地刪除、標記為已完成、按讚等等。

擷取程式 允許你與路由 動作載入程式 互動,而不會在瀏覽器中造成導航,但仍然獲得所有常規的好處,例如錯誤處理、重新驗證、中斷處理和競爭條件處理。

想像一下一個任務串列

function Tasks() {
  const tasks = useLoaderData();
  return tasks.map((task) => (
    <div>
      <p>{task.name}</p>
      <ToggleCompleteButton task={task} />
    </div>
  ));
}

每個任務都可以在獨立於其他任務的狀況下標記為「已完成」,具有自己的協力狀態,並且不會造成 擷取程式 導航

function ToggleCompleteButton({ task }) {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post" action="/toggle-complete">
      <fieldset disabled={fetcher.state !== "idle"}>
        <input type="hidden" name="id" value={task.id} />
        <input
          type="hidden"
          name="status"
          value={task.complete ? "incomplete" : "complete"}
        />
        <button type="submit">
          {task.status === "complete"
            ? "Mark Incomplete"
            : "Mark Complete"}
        </button>
      </fieldset>
    </fetcher.Form>
  );
}

請參閱

競爭條件處理

React Router 會自動取消過期的操作,並且只提交最新的資料。

任何時候當你有非同步 UI 時,都會有競爭條件的風險:當一個非同步操作在另一個較早的操作之後開始,卻在該操作之前完成。結果就是使用者介面顯示錯誤的狀態。

考慮一個在使用者輸入時更新串列的搜尋欄位

?q=ry    |---------------|
                         ^ commit wrong state
?q=ryan     |--------|
                     ^ lose correct state

儘管查詢 q?=ryan 後發送,但它完成得較早。如果處理不當,結果簡短地顯示為 ?q=ryan 的正確值,但接著會反轉為 ?q=ry 的錯誤結果。限制頻率和防反彈不足以應付(你仍然可以中斷通過的請求)。你需要取消功能。

如果你使用 React Router 的資料慣例,你可以完全自動避免這個問題。

?q=ry    |-----------X
                     ^ cancel wrong state when
                       correct state completes earlier
?q=ryan     |--------|
                     ^ commit correct state

React Router 不僅處理此類導航的競爭情況,還處理許多其他案例,例如載入自動完成結果或使用 fetcher 執行多個並發突變(以及其自動、並發重新驗證)。

錯誤處理

React Router 會自動處理絕大多數應用程式錯誤。它會在以下情況下捕捉任何錯誤:

  • 渲染
  • 載入資料
  • 更新資料

實際上,除了在事件處理常式(<button onClick>)或 useEffect 中引發的錯誤外,這幾乎涵蓋了你應用程式中的所有錯誤。React Router 應用程式中這兩類錯誤都很少。

引發錯誤時,會渲染 errorElement,而不是渲染路徑的 element

<Route
  path="/"
  loader={() => {
    something.that.throws.an.error();
  }}
  // this will not be rendered
  element={<HappyPath />}
  // but this will instead
  errorElement={<ErrorBoundary />}
/>

如果路徑沒有 errorElement,錯誤會冒泡到具有 errorElement 的最接近父路徑。

<Route
  path="/"
  element={<HappyPath />}
  errorElement={<ErrorBoundary />}
>
  {/* Errors here bubble up to the parent route */}
  <Route path="login" element={<Login />} />
</Route>

請參閱

捲動復原

React Router 會在導航時模擬瀏覽器的捲動復原,並在捲動前等待資料載入。這可確保捲動位置復原到正確的位置。

你也可以根據位置以外的內容(例如 URL 路徑名稱)復原,並阻止在特定連結(例如頁面中間的標籤)發生捲動,以自訂行為。

請參閱

Web 標準 API

React Router 建立在 Web 標準 API 之上。加載器動作接收標準 Web Fetch API Request 物件,也可以傳回 Response 物件。取消使用 中止訊號 執行,搜尋參數使用 URLSearchParams 處理,資料突變使用 HTML 表單 處理。

當你對 React Router 愈上手,對於 Web 平台就愈上手。

搜尋參數

待辦

位置狀態

待辦