web-routing-react-router

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Router Patterns

React Router 模式

Quick Guide: This skill covers React Router v7 Data Mode —
createBrowserRouter
plus loaders, actions, fetchers and pending states, without a full framework. Four v7 facts change the answer: every export comes from
"react-router"
, with the DOM-only ones —
RouterProvider
included — also on
"react-router/dom"
, while
react-router-dom
survives v7 as a deprecated re-export and is deleted in v8;
json()
and
defer()
are removed so loaders return plain objects; form method values are uppercase (
"POST"
); and loaders skip revalidation after an action error unless
shouldRevalidate
opts back in.
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>
    ,
    useFetcher
    , optimistic UI,
    shouldRevalidate
    , deferred data
  • examples/navigation.md —
    Link
    ,
    NavLink
    ,
    useNavigate
    ,
    redirect
    ,
    useSearchParams
    wired to loaders
  • examples/error-handling.md — error bubbling, thrown responses, pending UI,
    route.lazy
  • examples/layouts.md —
    Outlet
    ,
    useOutletContext
    , pathless auth layouts,
    useBlocker
  • reference.md — route options, hook and component tables,
    createBrowserRouter
    options

快速指南: 本 skill 涵盖 React Router v7 Data Mode——
createBrowserRouter
加上 loader、action、fetcher 和 pending 状态,无需完整框架。v7 中有四项关键变化会影响具体做法:所有导出都来自
"react-router"
,仅限 DOM 的导出(其中包括
RouterProvider
)也可从
"react-router/dom"
导入,而
react-router-dom
在 v7 中仍作为已弃用的 re-export 存在,并将在 v8 中删除;
json()
和
defer()
已被移除,因此 loader 返回普通对象;表单 method 值为大写(
"POST"
);并且在 action 发生错误后,loader 会跳过重新验证,除非通过
shouldRevalidate
显式选择重新验证。
详细资源:
  • examples/core.md——路由设置、根布局、根错误边界,以及两个 v7 迁移陷阱
  • examples/data-loading.md——loader、action、
    <Form>
    、
    useFetcher
    、乐观 UI、
    shouldRevalidate
    、延迟数据
  • examples/navigation.md——
    Link
    、
    NavLink
    、
    useNavigate
    、
    redirect
    、
    useSearchParams
    与 loader 的联动方式
  • examples/error-handling.md——错误冒泡、抛出的 Response、pending UI、
    route.lazy
  • examples/layouts.md——
    Outlet
    、
    useOutletContext
    、无路径的 auth 布局、
    useBlocker
  • reference.md——路由选项、hook 和组件对照表、
    createBrowserRouter
    选项

Which path applies

选择适用模式

React Router ships three modes, and only one of them has the data APIs.
  • Data Mode —
    createBrowserRouter
    +
    <RouterProvider>
    . Loaders, actions, fetchers and
    errorElement
    all work. This is the skill; start at examples/core.md.
  • Declarative Mode —
    <BrowserRouter>
    +
    <Routes>
    . URL matching and
    <Link>
    only. A
    loader
    prop here is silently ignored, so reach for it when there is no data to load and nothing in this skill's patterns applies.
  • 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
    +
    <RouterProvider>
    。loader、action、fetcher 和
    errorElement
    均可使用。本 skill 讲的就是这一模式;从 examples/core.md 开始。
  • Declarative Mode——
    <BrowserRouter>
    +
    <Routes>
    。仅支持 URL 匹配和
    <Link>
    。这里的
    loader
    prop 会被静默忽略,因此仅在没有数据需要加载、且本 skill 中的模式均不适用时使用它。
  • Framework Mode——通过 router 自带的 bundler plugin 实现基于文件的路由、SSR 和流式传输。它属于另一套使用面,有自己的约定,不在本 skill 的覆盖范围内。

<critical_requirements>

Before writing React Router code

编写 React Router 代码前

Import everything from
"react-router"
, and
RouterProvider
from
"react-router/dom"
— that copy wires up
react-dom
's
flushSync
.
react-router-dom
still installs in v7 as a re-export, so a stale import fails no build and nothing tells you; v8 deletes the package.
Reach for
createBrowserRouter
+
<RouterProvider>
whenever a loader, action or fetcher is involved.
Those are Data Mode features; under
<BrowserRouter>
the props are accepted and ignored, with no error to tell you.
Return plain objects from loaders, or a
Response
you built yourself.
json()
and
defer()
were removed in v7.
Use
throw redirect()
rather than
return redirect()
.
Throwing unwinds the whole call stack, which is what makes a shared
requireAuth()
helper stop the loader that called it.
Give the root route an
errorElement
or
ErrorBoundary
.
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.
</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 —
    NavLink
    hands its state to a
    className
    or
    style
    function, and what those return is not its concern.
  • 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 (
loader
), what mutations it accepts (
action
), and what catches failures (
errorElement
) — all before its component renders. That moves orchestration out of components and removes the fetch-on-mount waterfall.
  • 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
    errorElement
    , leaving parent layouts on screen.
  • Revalidation is automatic. A successful action re-runs every active loader, so there is no cache to invalidate by hand.
</philosophy>
<decision_framework>
所有内容都从
"react-router"
导入,而
RouterProvider
从
"react-router/dom"
导入
——后者会接入
react-dom
的
flushSync
。
react-router-dom
在 v7 中仍可安装并作为 re-export 使用,因此过时的导入不会导致构建失败,也没有任何提示;v8 将删除该包。
只要涉及 loader、action 或 fetcher,就使用
createBrowserRouter
+
<RouterProvider>
。
这些都是 Data Mode 功能;在
<BrowserRouter>
下,相关 props 会被接受并忽略,且不会报错。
loader 应返回普通对象,或自行构造的
Response
。
json()
和
defer()
在 v7 中已被移除。
使用
throw redirect()
,而不是
return redirect()
。
抛出操作会展开整个调用栈,这正是共享的
requireAuth()
helper 能中止调用它的 loader 的方式。
为根路由提供
errorElement
或
ErrorBoundary
。
它是路由树中的最后一道兜底;如果没有它,单个 loader 失败就会用 router 内置的错误转储页面替换整个应用。
</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。每条路由都会在组件渲染前声明要加载什么(
loader
)、接受哪些 mutation(
action
),以及用什么捕获失败(
errorElement
)。这样可以把编排逻辑移出组件,并消除挂载时请求形成的 waterfall。
  • 路由拥有自己的数据。 组件负责接收数据,而不负责获取数据。
  • URL 是唯一事实来源。 路径参数、搜索参数和导航状态都存放在 URL 中。
  • 错误会冒泡。 错误会向上传递到最近的
    errorElement
    ,同时让父布局继续显示在屏幕上。
  • 重新验证是自动的。 成功的 action 会重新运行所有活跃 loader,因此无需手动使缓存失效。
</philosophy>
<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 object
The 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
errorElement
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
Response
from a loader (
throw new Response("Not Found", { status: 404 })
) to make the failure an HTTP error that
isRouteErrorResponse
can narrow.
</decision_framework>

<patterns>
在根路由上放置
errorElement
以保证始终有兜底;对于失败需要专门 UI 的具体路由,也应分别放置
errorElement
——缺少一篇 post 与 dashboard 加载失败的呈现方式不同。从 loader 抛出
Response
(
throw new Response("Not Found", { status: 404 })
),可将失败转为 HTTP error,以便
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
{ request, params }
, where
request
is a standard Web
Request
. Sibling loaders run in parallel, parent before child.
typescript
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 处理提交。两者都接收
{ request, params }
,其中
request
是标准 Web
Request
。同级 loader 会并行运行,父级先于子级运行。
typescript
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 实现错误边界

errorElement
catches failures from the route's loader, action and component alike.
isRouteErrorResponse
separates a thrown
Response
from an unexpected exception.
typescript
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

errorElement
可捕获该路由的 loader、action 和组件产生的失败。
isRouteErrorResponse
用于区分抛出的
Response
与意外异常。
typescript
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
<Outlet />
where its matched child goes. The layout survives child navigation, so sidebars and their state persist.
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
is
"idle"
,
"loading"
(a loader is running) or
"submitting"
(an action is). One indicator in the root layout covers the whole app.
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"
(loader 正在运行)或
"submitting"
(action 正在运行)。在根布局中设置一个指示器即可覆盖整个应用。
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.Form
submits to an action without changing the URL. Fetchers carry independent
state
and
data
, several can be in flight at once, and active loaders still revalidate when each one finishes.
typescript
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.Form
会向 action 提交数据,而不改变 URL。Fetcher 拥有独立的
state
和
data
,可以同时有多个请求在途;每个请求完成后,活跃 loader 仍会重新验证。
typescript
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.lazy
defers a route module until navigation. The function form ships one chunk; the v7.5+ object form splits each property so the loader and the component download in parallel.
typescript
{ 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.lazy
将路由模块延迟到导航时再加载。函数形式会打包为一个 chunk;v7.5+ 的对象形式会拆分各个属性,使 loader 与组件并行下载。
typescript
{ 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
    "react-router-dom"
    — deprecated in v7 and deleted in v8, and it still resolves today so nothing tells you — import from
    "react-router"
    , or
    "react-router/dom"
    for
    RouterProvider
    .
  • Calling
    json()
    or
    defer()
    — removed in v7 — return a plain object, or a promise for the deferred half and read it with
    <Await>
    .
  • loader
    or
    action
    on a
    <Route>
    under
    <BrowserRouter>
    — accepted and ignored with no warning — switch the tree to
    createBrowserRouter
    .
  • No
    errorElement
    anywhere in the tree — one loader failure replaces the app with the router's default error dump — put one on the root.
  • return redirect()
    inside a shared helper — the caller carries on with a
    Response
    as its return value —
    throw
    it instead.
  • A layout that renders no
    <Outlet />
    — matched children render nowhere, silently.
  • useLoaderData()
    in a component whose route has no loader — returns
    undefined
    well away from the cause.
  • navigation.formMethod === "post"
    — v7 uppercases form methods — compare against
    "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
    shouldRevalidate
    opts back in.
  • useFetcher
    never moves
    useNavigation
    state; the two are independent.
  • The
    useSearchParams
    setter does not queue like
    setState
    — two calls in one tick do not build on each other, so use the callback form.
  • route.lazy
    cannot supply
    path
    ,
    index
    ,
    children
    or
    id
    ; those stay in the static config.
  • params
    values are always strings — parse in the loader rather than in the component.
  • Returning data from an action instead of redirecting leaves the result unaddressable, so a refresh shows the pre-mutation UI.
  • navigate(-1)
    assumes a history entry that a deep-linked visitor does not have.
</red_flags>
运行时会出错:
  • 从
    "react-router-dom"
    导入——在 v7 中已弃用,并会在 v8 中删除;目前它仍可解析,因此不会给出任何提示——应从
    "react-router"
    导入,
    RouterProvider
    则从
    "react-router/dom"
    导入。
  • 调用
    json()
    或
    defer()
    ——v7 中已移除——应返回普通对象,或为 deferred 部分返回一个 promise,并通过
    <Await>
    读取。
  • 在
    <BrowserRouter>
    下的
    <Route>
    上设置
    loader
    或
    action
    ——会被接受并忽略,且没有警告——应将路由树切换到
    createBrowserRouter
    。
  • 路由树中任何位置都没有
    errorElement
    ——单个 loader 失败会用 router 的默认错误转储页面替换整个应用——应在根路由上设置一个。
  • 在共享 helper 中使用
    return redirect()
    ——调用方会继续执行,并携带一个
    Response
    作为返回值——应改为
    throw
    。
  • 布局未渲染
    <Outlet />
    ——匹配到的子路由将无处渲染,而且不会报错。
  • 在路由没有 loader 的组件中使用
    useLoaderData()
    ——会返回
    undefined
    ,表面上很难看出根因。
  • navigation.formMethod === "post"
    ——v7 会将表单 method 转为大写——应与
    "POST"
    比较。
出人意料的行为:
  • 成功的 action 之后,所有活跃 loader 都会重新验证,而不只是执行该 action 的路由。action 发生_错误_后,所有 loader 都不会重新验证,除非
    shouldRevalidate
    显式选择重新验证。
  • useFetcher
    从不改变
    useNavigation
    状态;二者相互独立。
  • useSearchParams
    的 setter 不会像
    setState
    那样排队——同一 tick 内的两次调用不会基于彼此累积,因此应使用回调形式。
  • route.lazy
    不能提供
    path
    、
    index
    、
    children
    或
    id
    ;这些仍应保留在静态配置中。
  • params
    的值始终是字符串——应在 loader 中解析,而不是在组件中解析。
  • 从 action 返回数据而不重定向,会使结果无法寻址,因此刷新后会显示 mutation 之前的 UI。
  • navigate(-1)
    假定存在 history entry,而通过 deep link 访问的访客没有该记录。
</red_flags>