web-routing-react-router
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Router Patterns
React Router 模式
Quick Guide: This skill covers React Router v7 Data Mode —plus loaders, actions, fetchers and pending states, without a full framework. Four v7 facts change the answer: every export comes fromcreateBrowserRouter, with the DOM-only ones —"react-router"included — also onRouterProvider, while"react-router/dom"survives v7 as a deprecated re-export and is deleted in v8;react-router-domandjson()are removed so loaders return plain objects; form method values are uppercase (defer()); and loaders skip revalidation after an action error unless"POST"opts back in.shouldRevalidate
Detailed Resources:
- examples/core.md — router setup, root layout, root error boundary, and the two v7 migration traps
- examples/data-loading.md — loaders, actions, ,
<Form>, optimistic UI,useFetcher, deferred datashouldRevalidate - examples/navigation.md — ,
Link,NavLink,useNavigate,redirectwired to loadersuseSearchParams - examples/error-handling.md — error bubbling, thrown responses, pending UI,
route.lazy - examples/layouts.md — ,
Outlet, pathless auth layouts,useOutletContextuseBlocker - reference.md — route options, hook and component tables, options
createBrowserRouter
快速指南: 本 skill 涵盖 React Router v7 Data Mode——加上 loader、action、fetcher 和 pending 状态,无需完整框架。v7 中有四项关键变化会影响具体做法:所有导出都来自createBrowserRouter,仅限 DOM 的导出(其中包括"react-router")也可从RouterProvider导入,而"react-router/dom"在 v7 中仍作为已弃用的 re-export 存在,并将在 v8 中删除;react-router-dom和json()已被移除,因此 loader 返回普通对象;表单 method 值为大写(defer());并且在 action 发生错误后,loader 会跳过重新验证,除非通过"POST"显式选择重新验证。shouldRevalidate
详细资源:
- examples/core.md——路由设置、根布局、根错误边界,以及两个 v7 迁移陷阱
- examples/data-loading.md——loader、action、、
<Form>、乐观 UI、useFetcher、延迟数据shouldRevalidate - examples/navigation.md——、
Link、NavLink、useNavigate、redirect与 loader 的联动方式useSearchParams - examples/error-handling.md——错误冒泡、抛出的 Response、pending UI、
route.lazy - examples/layouts.md——、
Outlet、无路径的 auth 布局、useOutletContextuseBlocker - reference.md——路由选项、hook 和组件对照表、选项
createBrowserRouter
Which path applies
选择适用模式
React Router ships three modes, and only one of them has the data APIs.
- Data Mode — +
createBrowserRouter. Loaders, actions, fetchers and<RouterProvider>all work. This is the skill; start at examples/core.md.errorElement - Declarative Mode — +
<BrowserRouter>. URL matching and<Routes>only. A<Link>prop here is silently ignored, so reach for it when there is no data to load and nothing in this skill's patterns applies.loader - Framework Mode — file-based routes, SSR and streaming through the router's own bundler plugin. A separate surface with its own conventions, not covered here.
<critical_requirements>
React Router 提供三种模式,其中只有一种支持数据 API。
- Data Mode——+
createBrowserRouter。loader、action、fetcher 和<RouterProvider>均可使用。本 skill 讲的就是这一模式;从 examples/core.md 开始。errorElement - Declarative Mode——+
<BrowserRouter>。仅支持 URL 匹配和<Routes>。这里的<Link>prop 会被静默忽略,因此仅在没有数据需要加载、且本 skill 中的模式均不适用时使用它。loader - Framework Mode——通过 router 自带的 bundler plugin 实现基于文件的路由、SSR 和流式传输。它属于另一套使用面,有自己的约定,不在本 skill 的覆盖范围内。
<critical_requirements>
Before writing React Router code
编写 React Router 代码前
Import everything from , and from — that copy wires up 's . still installs in v7 as a re-export, so a stale import fails no build and nothing tells you; v8 deletes the package.
"react-router"RouterProvider"react-router/dom"react-domflushSyncreact-router-domReach for + whenever a loader, action or fetcher is involved. Those are Data Mode features; under the props are accepted and ignored, with no error to tell you.
createBrowserRouter<RouterProvider><BrowserRouter>Return plain objects from loaders, or a you built yourself. and were removed in v7.
Responsejson()defer()Use rather than . Throwing unwinds the whole call stack, which is what makes a shared helper stop the loader that called it.
throw redirect()return redirect()requireAuth()Give the root route an or . It is the last catch in the tree; without one, a single loader failure replaces the app with the router's built-in error dump.
errorElementErrorBoundary</critical_requirements>
Auto-detection: createBrowserRouter, RouterProvider, useLoaderData, useActionData, useNavigation, useSearchParams, useFetcher, useRouteError, isRouteErrorResponse, useOutletContext, useRevalidator, useBlocker, Outlet, NavLink, errorElement, shouldRevalidate, route.lazy, HydrateFallback
Applies to:
- Route trees with data loading, form actions and pending states
- Nested layouts with persistent shared UI
- Route-level error boundaries and not-found handling
- URL search params as application state
- Non-navigating mutations — inline forms, toggles, auto-save
- Route-level code splitting
Handled elsewhere:
- Caching and deduplicating server data across routes — a loader fetches per navigation, and whatever owns data fetching decides what is cached between them.
- Client state that has no business in the URL — this skill settles URL-shaped state only.
- Styling active and pending links — hands its state to a
NavLinkorclassNamefunction, and what those return is not its concern.style - Server rendering, streaming HTML and file-based route generation.
<philosophy>
React Router v7 treats the router as a data layer rather than a URL matcher. A route declares what to load (), what mutations it accepts (), and what catches failures () — all before its component renders. That moves orchestration out of components and removes the fetch-on-mount waterfall.
loaderactionerrorElement- Routes own their data. Components receive it; they do not fetch it.
- The URL is the source of truth. Path params, search params and navigation state all live there.
- Errors bubble. An error rises to the nearest , leaving parent layouts on screen.
errorElement - Revalidation is automatic. A successful action re-runs every active loader, so there is no cache to invalidate by hand.
<decision_framework>
所有内容都从 导入,而 从 导入——后者会接入 的 。 在 v7 中仍可安装并作为 re-export 使用,因此过时的导入不会导致构建失败,也没有任何提示;v8 将删除该包。
"react-router"RouterProvider"react-router/dom"react-domflushSyncreact-router-dom只要涉及 loader、action 或 fetcher,就使用 + 。 这些都是 Data Mode 功能;在 下,相关 props 会被接受并忽略,且不会报错。
createBrowserRouter<RouterProvider><BrowserRouter>loader 应返回普通对象,或自行构造的 。 和 在 v7 中已被移除。
Responsejson()defer()使用 ,而不是 。 抛出操作会展开整个调用栈,这正是共享的 helper 能中止调用它的 loader 的方式。
throw redirect()return redirect()requireAuth()为根路由提供 或 。 它是路由树中的最后一道兜底;如果没有它,单个 loader 失败就会用 router 内置的错误转储页面替换整个应用。
errorElementErrorBoundary</critical_requirements>
自动检测: createBrowserRouter, RouterProvider, useLoaderData, useActionData, useNavigation, useSearchParams, useFetcher, useRouteError, isRouteErrorResponse, useOutletContext, useRevalidator, useBlocker, Outlet, NavLink, errorElement, shouldRevalidate, route.lazy, HydrateFallback
适用于:
- 带数据加载、表单 action 和 pending 状态的路由树
- 包含持久共享 UI 的嵌套布局
- 路由级错误边界和 not-found 处理
- 将 URL 搜索参数用作应用状态
- 不触发导航的 mutation——内联表单、开关、自动保存
- 路由级代码拆分
由其他环节处理:
- 跨路由缓存和去重服务器数据——loader 会在每次导航时发起获取,而跨路由缓存什么由数据获取的所有方决定。
- 与 URL 无关的客户端状态——本 skill 只处理适合由 URL 表达的状态。
- 对 active 和 pending 链接进行样式设置——会将状态传给
NavLink或className函数,而这些函数返回什么不在其职责范围内。style - 服务端渲染、流式 HTML 和基于文件的路由生成。
<philosophy>
React Router v7 将 router 视为数据层,而不是 URL matcher。每条路由都会在组件渲染前声明要加载什么()、接受哪些 mutation(),以及用什么捕获失败()。这样可以把编排逻辑移出组件,并消除挂载时请求形成的 waterfall。
loaderactionerrorElement- 路由拥有自己的数据。 组件负责接收数据,而不负责获取数据。
- URL 是唯一事实来源。 路径参数、搜索参数和导航状态都存放在 URL 中。
- 错误会冒泡。 错误会向上传递到最近的 ,同时让父布局继续显示在屏幕上。
errorElement - 重新验证是自动的。 成功的 action 会重新运行所有活跃 loader,因此无需手动使缓存失效。
<decision_framework>
Where the logic goes
逻辑放在哪里
What does this code do?
+-- Fetch data the component needs? -> loader (parallel with sibling loaders)
+-- Handle a form submission? -> action (reads FormData, returns errors or redirects)
+-- Mutate without changing the URL? -> useFetcher (independent state, runs concurrently)
+-- Gate access to a subtree? -> pathless layout route whose loader throws redirect()What does this code do?
+-- Fetch data the component needs? -> loader (parallel with sibling loaders)
+-- Handle a form submission? -> action (reads FormData, returns errors or redirects)
+-- Mutate without changing the URL? -> useFetcher (independent state, runs concurrently)
+-- Gate access to a subtree? -> pathless layout route whose loader throws redirect()How to navigate
如何导航
Where are you?
+-- In JSX, on something clickable? -> <Link> or <NavLink>
+-- In a handler, after a side effect? -> useNavigate()
+-- In a loader or action? -> throw redirect()
+-- Outside the React tree entirely? -> router.navigate() on the router objectThe last one is the only route out of a module that never renders — a fetch wrapper redirecting on a 401, for instance. The hooks all need a component.
Where are you?
+-- In JSX, on something clickable? -> <Link> or <NavLink>
+-- In a handler, after a side effect? -> useNavigate()
+-- In a loader or action? -> throw redirect()
+-- Outside the React tree entirely? -> router.navigate() on the router object最后一种方式是唯一一种从不渲染任何内容的模块中退出的途径——例如,在收到 401 时进行重定向的 fetch wrapper。所有 hooks 都需要组件。
Where to catch errors
在哪里捕获错误
Put on the root for a guaranteed catch, and on individual routes where the failure has its own UI — a missing post reads differently from a failed dashboard. Throw a from a loader () to make the failure an HTTP error that can narrow.
errorElementResponsethrow new Response("Not Found", { status: 404 })isRouteErrorResponse</decision_framework>
<patterns>
在根路由上放置 以保证始终有兜底;对于失败需要专门 UI 的具体路由,也应分别放置 ——缺少一篇 post 与 dashboard 加载失败的呈现方式不同。从 loader 抛出 (),可将失败转为 HTTP error,以便 缩小错误范围。
errorElementerrorElementResponsethrow new Response("Not Found", { status: 404 })isRouteErrorResponse</decision_framework>
<patterns>
Core patterns
核心模式
Pattern 1: Data Mode setup
模式 1:Data Mode 设置
Routes are objects, defined outside React so the router can run loaders before anything mounts.
typescript
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <RootError />,
children: [
{ index: true, element: <HomePage /> },
{ path: "posts", element: <PostsPage />, loader: postsLoader },
],
},
]);Full code: examples/core.md
路由是对象,定义在 React 外部,以便 router 在任何内容挂载前运行 loader。
typescript
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <RootError />,
children: [
{ index: true, element: <HomePage /> },
{ path: "posts", element: <PostsPage />, loader: postsLoader },
],
},
]);完整代码:examples/core.md
Pattern 2: Loaders and actions
模式 2:Loader 和 action
A loader runs before render; an action handles submissions. Both receive , where is a standard Web . Sibling loaders run in parallel, parent before child.
{ request, params }requestRequesttypescript
export async function postsLoader({ request }: LoaderFunctionArgs) {
const response = await fetch("/api/posts");
if (!response.ok) throw new Response("Failed to load", { status: 500 });
return { posts: await response.json() };
}
export async function createPostAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = await createPost({ title: formData.get("title") });
throw redirect(`/posts/${post.id}`);
}Full code: examples/data-loading.md
loader 在渲染前运行;action 处理提交。两者都接收 ,其中 是标准 Web 。同级 loader 会并行运行,父级先于子级运行。
{ request, params }requestRequesttypescript
export async function postsLoader({ request }: LoaderFunctionArgs) {
const response = await fetch("/api/posts");
if (!response.ok) throw new Response("Failed to load", { status: 500 });
return { posts: await response.json() };
}
export async function createPostAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const post = await createPost({ title: formData.get("title") });
throw redirect(`/posts/${post.id}`);
}完整代码:examples/data-loading.md
Pattern 3: Error boundaries with errorElement
模式 3:使用 errorElement 实现错误边界
errorElementisRouteErrorResponseResponsetypescript
function RouteError() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <p role="alert">{error.status}: {error.statusText}</p>;
}
return <p role="alert">{error instanceof Error ? error.message : "Unknown error"}</p>;
}Full code: examples/error-handling.md
errorElementisRouteErrorResponseResponsetypescript
function RouteError() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return <p role="alert">{error.status}: {error.statusText}</p>;
}
return <p role="alert">{error instanceof Error ? error.message : "Unknown error"}</p>;
}完整代码:examples/error-handling.md
Pattern 4: Nested layouts with Outlet
模式 4:使用 Outlet 构建嵌套布局
A parent route renders where its matched child goes. The layout survives child navigation, so sidebars and their state persist.
<Outlet />typescript
function DashboardLayout() {
return (
<div>
<nav><NavLink to="/dashboard" end>Overview</NavLink></nav>
<main><Outlet /></main>
</div>
);
}Full code: examples/layouts.md
父路由会在匹配到的子路由所在位置渲染 。布局会在子路由导航时保留下来,因此侧边栏及其状态得以持久保留。
<Outlet />typescript
function DashboardLayout() {
return (
<div>
<nav><NavLink to="/dashboard" end>Overview</NavLink></nav>
<main><Outlet /></main>
</div>
);
}完整代码:examples/layouts.md
Pattern 5: Navigation state with useNavigation
模式 5:使用 useNavigation 跟踪导航状态
navigation.state"idle""loading""submitting"typescript
function GlobalSpinner() {
const navigation = useNavigation();
if (navigation.state === "idle") return null;
return <div role="progressbar" aria-busy="true" />;
}Full code: examples/error-handling.md
navigation.state"idle""loading""submitting"typescript
function GlobalSpinner() {
const navigation = useNavigation();
if (navigation.state === "idle") return null;
return <div role="progressbar" aria-busy="true" />;
}完整代码:examples/error-handling.md
Pattern 6: Non-navigating mutations with useFetcher
模式 6:使用 useFetcher 执行不触发导航的 mutation
fetcher.Formstatedatatypescript
function DeleteButton({ postId }: { postId: string }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="POST" action={`/posts/${postId}/delete`}>
<button type="submit" disabled={fetcher.state !== "idle"}>Delete</button>
</fetcher.Form>
);
}Full code: examples/data-loading.md
fetcher.Formstatedatatypescript
function DeleteButton({ postId }: { postId: string }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="POST" action={`/posts/${postId}/delete`}>
<button type="submit" disabled={fetcher.state !== "idle"}>Delete</button>
</fetcher.Form>
);
}完整代码:examples/data-loading.md
Pattern 7: Code splitting with route.lazy
模式 7:使用 route.lazy 进行代码拆分
route.lazytypescript
{ path: "admin", lazy: () => import("./pages/admin") }
{
path: "admin",
lazy: {
loader: async () => (await import("./pages/admin.loader")).loader,
Component: async () => (await import("./pages/admin.component")).AdminPage,
},
}Full code: examples/error-handling.md
</patterns>
<red_flags>
route.lazytypescript
{ path: "admin", lazy: () => import("./pages/admin") }
{
path: "admin",
lazy: {
loader: async () => (await import("./pages/admin.loader")).loader,
Component: async () => (await import("./pages/admin.component")).AdminPage,
},
}完整代码:examples/error-handling.md
</patterns>
<red_flags>
Red flags
危险信号
Breaks at runtime:
- Importing from — deprecated in v7 and deleted in v8, and it still resolves today so nothing tells you — import from
"react-router-dom", or"react-router"for"react-router/dom".RouterProvider - Calling or
json()— removed in v7 — return a plain object, or a promise for the deferred half and read it withdefer().<Await> - or
loaderon aactionunder<Route>— accepted and ignored with no warning — switch the tree to<BrowserRouter>.createBrowserRouter - No anywhere in the tree — one loader failure replaces the app with the router's default error dump — put one on the root.
errorElement - inside a shared helper — the caller carries on with a
return redirect()as its return value —Responseit instead.throw - A layout that renders no — matched children render nowhere, silently.
<Outlet /> - in a component whose route has no loader — returns
useLoaderData()well away from the cause.undefined - — v7 uppercases form methods — compare against
navigation.formMethod === "post"."POST"
Surprising behaviour:
- After a successful action every active loader revalidates, not just the acting route's. After an action error none of them do, unless opts back in.
shouldRevalidate - never moves
useFetcherstate; the two are independent.useNavigation - The setter does not queue like
useSearchParams— two calls in one tick do not build on each other, so use the callback form.setState - cannot supply
route.lazy,path,indexorchildren; those stay in the static config.id - values are always strings — parse in the loader rather than in the component.
params - Returning data from an action instead of redirecting leaves the result unaddressable, so a refresh shows the pre-mutation UI.
- assumes a history entry that a deep-linked visitor does not have.
navigate(-1)
</red_flags>
运行时会出错:
- 从 导入——在 v7 中已弃用,并会在 v8 中删除;目前它仍可解析,因此不会给出任何提示——应从
"react-router-dom"导入,"react-router"则从RouterProvider导入。"react-router/dom" - 调用 或
json()——v7 中已移除——应返回普通对象,或为 deferred 部分返回一个 promise,并通过defer()读取。<Await> - 在 下的
<BrowserRouter>上设置<Route>或loader——会被接受并忽略,且没有警告——应将路由树切换到action。createBrowserRouter - 路由树中任何位置都没有 ——单个 loader 失败会用 router 的默认错误转储页面替换整个应用——应在根路由上设置一个。
errorElement - 在共享 helper 中使用 ——调用方会继续执行,并携带一个
return redirect()作为返回值——应改为Response。throw - 布局未渲染 ——匹配到的子路由将无处渲染,而且不会报错。
<Outlet /> - 在路由没有 loader 的组件中使用 ——会返回
useLoaderData(),表面上很难看出根因。undefined - ——v7 会将表单 method 转为大写——应与
navigation.formMethod === "post"比较。"POST"
出人意料的行为:
- 成功的 action 之后,所有活跃 loader 都会重新验证,而不只是执行该 action 的路由。action 发生_错误_后,所有 loader 都不会重新验证,除非 显式选择重新验证。
shouldRevalidate - 从不改变
useFetcher状态;二者相互独立。useNavigation - 的 setter 不会像
useSearchParams那样排队——同一 tick 内的两次调用不会基于彼此累积,因此应使用回调形式。setState - 不能提供
route.lazy、path、index或children;这些仍应保留在静态配置中。id - 的值始终是字符串——应在 loader 中解析,而不是在组件中解析。
params - 从 action 返回数据而不重定向,会使结果无法寻址,因此刷新后会显示 mutation 之前的 UI。
- 假定存在 history entry,而通过 deep link 访问的访客没有该记录。
navigate(-1)
</red_flags>