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}`);
}}
/>
請參閱
當使用者在應用程式中導覽時,下一個頁面的資料會在頁面渲染前載入。這時提供使用者回饋資料很重要,否則應用程式會感覺沒有回應。
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>
);
}
請參閱
了解即將傳送至動作 的 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 按鈕可以具有 name
和 value
)。
雖然使用 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 路徑名稱)復原,並阻止在特定連結(例如頁面中間的標籤)發生捲動,以自訂行為。
請參閱
React Router 建立在 Web 標準 API 之上。加載器和動作接收標準 Web Fetch API Request
物件,也可以傳回 Response
物件。取消使用 中止訊號 執行,搜尋參數使用 URLSearchParams
處理,資料突變使用 HTML 表單 處理。
當你對 React Router 愈上手,對於 Web 平台就愈上手。