react-router-7-framework

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Router 7 Framework Mode Best Practices

React Router 7框架模式最佳实践

Version Compatibility

版本兼容性

This skill targets React Router 7.9.0+ in framework mode. Key features by version:
VersionFeatures
v7.0Framework mode, type generation, loaders/actions,
Route.*
types
v7.5
href()
utility for type-safe links
v7.9+Stable middleware and context APIs, v8 future flags
本指南针对框架模式下的**React Router 7.9.0+**版本。各版本关键特性:
版本特性
v7.0框架模式、类型生成、loaders/actions、
Route.*
类型
v7.5用于类型安全链接的
href()
工具函数
v7.9+稳定的中间件和上下文API、v8未来特性标志

Future Flags

未来特性标志

Enable v8 features in
react-router.config.ts
:
typescript
import type { Config } from "@react-router/dev/config";

export default {
  future: {
    v8_middleware: true,        // Middleware support
    v8_splitRouteModules: true, // Route module splitting for performance
  },
} satisfies Config;
These will become the default in v8.
react-router.config.ts
中启用v8特性:
typescript
import type { Config } from "@react-router/dev/config";

export default {
  future: {
    v8_middleware: true,        // 中间件支持
    v8_splitRouteModules: true, // 路由模块拆分以提升性能
  },
} satisfies Config;
这些特性将在v8版本中成为默认配置。

Core Principles

核心原则

  1. Server-First: Fetch and process data on the server whenever possible. Only use client-side data fetching when absolutely necessary.
  2. Maximum Type Safety: Always use React Router 7's generated types (
    Route.LoaderArgs
    ,
    Route.ComponentProps
    , etc.). Access loader/action data from props or use
    typeof loader
    /
    typeof action
    with hooks.
  3. Proper Hydration: Understand when to use
    clientLoader.hydrate = true
    and when to skip it
  4. Declarative Data: Colocate data requirements with routes using loaders
  5. Progressive Enhancement: Use actions for mutations with automatic revalidation
  1. 服务端优先:尽可能在服务端获取和处理数据。仅在绝对必要时使用客户端数据获取。
  2. 最大化类型安全:始终使用React Router 7生成的类型(
    Route.LoaderArgs
    Route.ComponentProps
    等)。通过props或结合钩子使用
    typeof loader
    /
    typeof action
    来访问loader/action的数据。
  3. 合适的Hydration:了解何时使用
    clientLoader.hydrate = true
    以及何时跳过它
  4. 声明式数据:使用loaders将数据需求与路由进行内聚管理
  5. 渐进式增强:使用actions处理变更并自动重新验证数据

Route Configuration

路由配置

Define routes in
app/routes.ts
using helper functions:
typescript
import type { RouteConfig } from "@react-router/dev/routes";
import { route, index, layout, prefix } from "@react-router/dev/routes";

export default [
  index("./home.tsx"),
  route("about", "./about.tsx"),
  layout("./auth-layout.tsx", [
    route("login", "./login.tsx"),
    route("register", "./register.tsx"),
  ]),
  ...prefix("api", [
    route("users", "./api/users.tsx"),
  ]),
  route("*", "./not-found.tsx"), // Catch-all 404
] satisfies RouteConfig;
See references/routes-config.md for layout routes, Outlet, splat routes, custom IDs, and nested route patterns.
使用辅助函数在
app/routes.ts
中定义路由:
typescript
import type { RouteConfig } from "@react-router/dev/routes";
import { route, index, layout, prefix } from "@react-router/dev/routes";

export default [
  index("./home.tsx"),
  route("about", "./about.tsx"),
  layout("./auth-layout.tsx", [
    route("login", "./login.tsx"),
    route("register", "./register.tsx"),
  ]),
  ...prefix("api", [
    route("users", "./api/users.tsx"),
  ]),
  route("*", "./not-found.tsx"), // 全局404捕获
] satisfies RouteConfig;
有关布局路由、Outlet、通配符路由、自定义ID和嵌套路由模式,请参阅references/routes-config.md

Type Safety

类型安全

Generated Types

生成的类型

React Router 7 generates route-specific types in
.react-router/types/+types/<route-file>.d.ts
for each route. Always import and use these types:
typescript
import type { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  // params is typed based on your route definition
  const product = await db.getProduct(params.id);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  // loaderData is inferred from loader return type
  return <h1>{loaderData.product.name}</h1>;
}
React Router 7会为每个路由在
.react-router/types/+types/<route-file>.d.ts
中生成路由特定的类型。请始终导入并使用这些类型:
typescript
import type { Route } from "./+types/product";

export async function loader({ params }: Route.LoaderArgs) {
  // params根据路由定义自动生成类型
  const product = await db.getProduct(params.id);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  // loaderData的类型由loader的返回值自动推断
  return <h1>{loaderData.product.name}</h1>;
}

Available Route Types

可用的路由类型

  • Route.LoaderArgs
    - Types for loader parameters (params, request, context)
  • Route.ActionArgs
    - Types for action parameters
  • Route.ClientLoaderArgs
    - Types for clientLoader parameters (includes serverLoader)
  • Route.ClientActionArgs
    - Types for clientAction parameters (includes serverAction)
  • Route.ComponentProps
    - Types for component props (includes loaderData, actionData, matches, etc.)
  • Route.LoaderArgs
    - loader参数的类型(params、request、context)
  • Route.ActionArgs
    - action参数的类型
  • Route.ClientLoaderArgs
    - clientLoader参数的类型(包含serverLoader)
  • Route.ClientActionArgs
    - clientAction参数的类型(包含serverAction)
  • Route.ComponentProps
    - 组件props的类型(包含loaderData、actionData、matches等)

Accessing Loader/Action Data

访问Loader/Action数据

In route module default exports, always use props — they provide the best type safety and are the recommended approach in framework mode:
typescript
import type { Route } from "./+types/product";

export async function loader() {
  return { product: await db.getProduct() };
}

export async function action() {
  return { success: true };
}

// ✅ Props are auto-typed for this specific route
export default function Product({
  loaderData,
  actionData,
}: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
When to use hooks instead:
Hooks (
useLoaderData
,
useActionData
) are for non-route-module contexts — deep child components, shared UI, or when testing:
typescript
// In a child component that doesn't have direct access to route props
import { useLoaderData } from "react-router";

function ProductDetails() {
  // Use typeof for type inference
  const { product } = useLoaderData<typeof import("./route").loader>();
  return <span>{product.description}</span>;
}
Note: Hook generics like
useLoaderData<typeof loader>()
exist largely for migration from Remix and are considered secondary to the props pattern. The
Route.*
types via props are the "most type-safe / least foot-gun" approach.
❌ Never use:
useLoaderData<Route.ComponentProps["loaderData"]>()
— this pattern is incorrect.
在路由模块的默认导出中,始终使用props——它们提供最佳的类型安全性,是框架模式中的推荐方式:
typescript
import type { Route } from "./+types/product";

export async function loader() {
  return { product: await db.getProduct() };
}

export async function action() {
  return { success: true };
}

// ✅ 针对该特定路由自动生成props类型
export default function Product({
  loaderData,
  actionData,
}: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
何时改用钩子:
钩子(
useLoaderData
useActionData
)适用于非路由模块上下文——深层子组件、共享UI或测试场景:
typescript
// 在无法直接访问路由props的子组件中
import { useLoaderData } from "react-router";

function ProductDetails() {
  // 使用typeof进行类型推断
  const { product } = useLoaderData<typeof import("./route").loader>();
  return <span>{product.description}</span>;
}
注意:像
useLoaderData<typeof loader>()
这样的钩子泛型主要用于从Remix迁移的场景,相对于props模式是次要选择。通过props使用
Route.*
类型是“最类型安全/最少陷阱”的方式。
❌ 切勿使用
useLoaderData<Route.ComponentProps["loaderData"]>()
——这种模式是错误的。

Type-Safe Links

类型安全链接

Use the
href
utility for type-safe route generation (v7.5+):
typescript
import { href } from "react-router";
import { Link, NavLink } from "react-router";

// Basic usage with params
<Link to={href("/products/:id", { id: "123" })} />

// Optional params
<NavLink to={href("/:lang?/about", { lang: "en" })} />

// No params needed
<Link to={href("/contact")} />

// Programmatic use
const productLink = href("/products/:id", { id: productId });
navigate(productLink);

// Type errors caught at compile time:
href("/not/a/valid/path");         // ❌ Error: Invalid path
href("/blog/:slug", { oops: 1 });  // ❌ Error: Invalid param name
href("/blog/:slug", {});           // ❌ Error: Missing required param
Benefits:
  • Compile-time validation of route paths
  • Required params are enforced
  • Refactoring routes updates all usages
  • IDE autocomplete for available routes
使用
href
工具函数生成类型安全的路由链接(v7.5+):
typescript
import { href } from "react-router";
import { Link, NavLink } from "react-router";

// 带参数的基础用法
<Link to={href("/products/:id", { id: "123" })} />

// 可选参数
<NavLink to={href("/:lang?/about", { lang: "en" })} />

// 无需参数
<Link to={href("/contact")} />

// 编程式使用
const productLink = href("/products/:id", { id: productId });
navigate(productLink);

// 编译时捕获类型错误:
href("/not/a/valid/path");         // ❌ 错误:无效路径
href("/blog/:slug", { oops: 1 });  // ❌ 错误:无效参数名称
href("/blog/:slug", {});           // ❌ 错误:缺少必填参数
优势
  • 路由路径的编译时验证
  • 必填参数强制校验
  • 重构路由时自动更新所有引用
  • IDE自动补全可用路由

Data Loading Patterns

数据加载模式

1. Server-Only Loading (Preferred)

1. 仅服务端加载(推荐)

Default pattern - load data on the server, hydrate automatically:
typescript
import type { Route } from "./+types/products";

export async function loader({ params, request }: Route.LoaderArgs) {
  // Runs on server during SSR and on server during client navigations
  const product = await db.getProduct(params.id);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
When to use: This is the default and preferred pattern. Use unless you have specific client-side requirements.
默认模式——在服务端加载数据,自动进行hydration:
typescript
import type { Route } from "./+types/products";

export async function loader({ params, request }: Route.LoaderArgs) {
  // 在SSR期间和客户端导航时均在服务端运行
  const product = await db.getProduct(params.id);
  return { product };
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
何时使用:这是默认且推荐的模式。除非有特定的客户端需求,否则请使用此模式。

2. Client-Only Loading

2. 仅客户端加载

Load data exclusively on the client:
typescript
import type { Route } from "./+types/products";

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
  // Only runs in the browser
  const res = await fetch(`/api/products/${params.id}`);
  return await res.json();
}

// Required when clientLoader runs during hydration
export function HydrateFallback() {
  return <div>Loading...</div>;
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.name}</div>;
}
When to use:
  • Accessing browser-only APIs (localStorage, IndexedDB)
  • Client-side caching strategies
  • No server environment available
Important:
clientLoader.hydrate = true
is implicit when no server
loader
exists.
仅在浏览器中加载数据:
typescript
import type { Route } from "./+types/products";

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
  // 仅在浏览器中运行
  const res = await fetch(`/api/products/${params.id}`);
  return await res.json();
}

// 当clientLoader在hydration期间运行时需要导出
export function HydrateFallback() {
  return <div>加载中...</div>;
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.name}</div>;
}
何时使用
  • 访问仅浏览器可用的API(localStorage、IndexedDB)
  • 客户端缓存策略
  • 无服务端环境可用
重要提示:当不存在服务端
loader
时,
clientLoader.hydrate = true
是隐式生效的。

3. Combined Server + Client Loading

3. 服务端+客户端组合加载

Augment server data with client data:
typescript
import type { Route } from "./+types/products";

export async function loader({ params }: Route.LoaderArgs) {
  // Server data (e.g., from database)
  return await db.getProduct(params.id);
}

export async function clientLoader({
  params,
  serverLoader,
}: Route.ClientLoaderArgs) {
  // Get server data + add client data
  const [serverData, clientData] = await Promise.all([
    serverLoader(),
    getClientOnlyData(params.id),
  ]);
  return { ...serverData, ...clientData };
}
clientLoader.hydrate = true as const; // Use 'as const' for proper type inference

export function HydrateFallback() {
  return <div>Loading...</div>;
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.name}</div>;
}
When to use:
  • Combining server data with client-only data (user preferences, client state)
  • Augmenting server data with cached data
Important: Set
clientLoader.hydrate = true as const
to call clientLoader during initial hydration.
用客户端数据增强服务端数据:
typescript
import type { Route } from "./+types/products";

export async function loader({ params }: Route.LoaderArgs) {
  // 服务端数据(例如来自数据库)
  return await db.getProduct(params.id);
}

export async function clientLoader({
  params,
  serverLoader,
}: Route.ClientLoaderArgs) {
  // 获取服务端数据并添加客户端数据
  const [serverData, clientData] = await Promise.all([
    serverLoader(),
    getClientOnlyData(params.id),
  ]);
  return { ...serverData, ...clientData };
}
clientLoader.hydrate = true as const; // 使用'as const'以获得正确的类型推断

export function HydrateFallback() {
  return <div>加载中...</div>;
}

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.name}</div>;
}
何时使用
  • 结合服务端数据与仅客户端可用数据(用户偏好、客户端状态)
  • 用缓存数据增强服务端数据
重要提示:设置
clientLoader.hydrate = true as const
以在初始hydration期间调用clientLoader。

4. Skip Server Hop (BFF Pattern)

4. 跳过服务端跳转(BFF模式)

Load server data on initial request, then call client API directly:
typescript
import type { Route } from "./+types/products";

export async function loader({ params }: Route.LoaderArgs) {
  // Server loads data on initial document request
  const product = await db.getProduct(params.id);
  return { product };
}

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
  // Subsequent navigations fetch from API directly (skip server hop)
  const res = await fetch(`/api/products/${params.id}`);
  return await res.json();
}
// clientLoader.hydrate is false (default) - only runs on subsequent navigations

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
When to use:
  • Backend-For-Frontend pattern
  • Initial SSR data load, then direct API calls
  • Authentication/cookies work for both server and client
Important: Do NOT set
clientLoader.hydrate = true
for this pattern. You want clientLoader to skip during hydration.
初始请求时加载服务端数据,后续直接调用客户端API:
typescript
import type { Route } from "./+types/products";

export async function loader({ params }: Route.LoaderArgs) {
  // 初始文档请求时服务端加载数据
  const product = await db.getProduct(params.id);
  return { product };
}

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
  // 后续导航直接从API获取数据(跳过服务端跳转)
  const res = await fetch(`/api/products/${params.id}`);
  return await res.json();
}
// clientLoader.hydrate默认为false - 仅在后续导航时运行

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
何时使用
  • 后端为前端(BFF)模式
  • 初始SSR数据加载,之后直接调用API
  • 认证/ Cookie在服务端和客户端均有效
重要提示:此模式下请勿设置
clientLoader.hydrate = true
。你希望clientLoader在hydration期间跳过执行。

5. Client-Side Caching

5. 客户端缓存

Cache server data on client for subsequent navigations:
typescript
import type { Route } from "./+types/products";

let isInitialRequest = true;
const cache = new Map();

export async function loader({ params }: Route.LoaderArgs) {
  return await db.getProduct(params.id);
}

export async function clientLoader({
  params,
  serverLoader,
}: Route.ClientLoaderArgs) {
  const cacheKey = `product-${params.id}`;

  // First request: prime cache
  if (isInitialRequest) {
    isInitialRequest = false;
    const data = await serverLoader();
    cache.set(cacheKey, data);
    return data;
  }

  // Subsequent requests: use cache
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const data = await serverLoader();
  cache.set(cacheKey, data);
  return data;
}
clientLoader.hydrate = true as const;

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.name}</div>;
}
When to use:
  • Optimizing for repeated visits to same routes
  • Reducing server round-trips
  • Offline-first strategies
在客户端缓存服务端数据以用于后续导航:
typescript
import type { Route } from "./+types/products";

let isInitialRequest = true;
const cache = new Map();

export async function loader({ params }: Route.LoaderArgs) {
  return await db.getProduct(params.id);
}

export async function clientLoader({
  params,
  serverLoader,
}: Route.ClientLoaderArgs) {
  const cacheKey = `product-${params.id}`;

  // 首次请求:填充缓存
  if (isInitialRequest) {
    isInitialRequest = false;
    const data = await serverLoader();
    cache.set(cacheKey, data);
    return data;
  }

  // 后续请求:使用缓存
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const data = await serverLoader();
  cache.set(cacheKey, data);
  return data;
}
clientLoader.hydrate = true as const;

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.name}</div>;
}
何时使用
  • 优化重复访问同一路由的场景
  • 减少服务端往返次数
  • 离线优先策略

Actions and Mutations

Actions与数据变更

Server Actions (Preferred)

服务端Actions(推荐)

Handle mutations on the server with automatic revalidation:
typescript
import type { Route } from "./+types/todos";
import { Form } from "react-router";

export async function loader() {
  // This runs after action completes
  const todos = await db.getTodos();
  return { todos };
}

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const title = formData.get("title");
  await db.createTodo({ title });
  return { success: true };
}

export default function Todos({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <ul>
        {loaderData.todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">Add Todo</button>
      </Form>
    </div>
  );
}
Key features:
  • Automatic revalidation of all loaders after action completes
  • Type-safe with
    Route.ActionArgs
  • Works with
    <Form>
    ,
    useFetcher
    , and
    useSubmit
在服务端处理数据变更,自动重新验证数据:
typescript
import type { Route } from "./+types/todos";
import { Form } from "react-router";

export async function loader() {
  // 此函数会在action完成后自动重新运行
  const todos = await db.getTodos();
  return { todos };
}

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const title = formData.get("title");
  await db.createTodo({ title });
  return { success: true };
}

export default function Todos({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <ul>
        {loaderData.todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
      <Form method="post">
        <input type="text" name="title" />
        <button type="submit">添加待办</button>
      </Form>
    </div>
  );
}
关键特性
  • action完成后自动重新验证所有loaders
  • 结合
    Route.ActionArgs
    实现类型安全
  • 支持
    <Form>
    useFetcher
    useSubmit

Client Actions

客户端Actions

Handle mutations in the browser, optionally calling server action:
typescript
import type { Route } from "./+types/todos";

export async function action({ request }: Route.ActionArgs) {
  // Server mutation
  const formData = await request.formData();
  await db.createTodo({ title: formData.get("title") });
  return { success: true };
}

export async function clientAction({
  request,
  serverAction,
}: Route.ClientActionArgs) {
  // Invalidate client cache first
  clientCache.invalidate();

  // Optionally call server action
  const result = await serverAction();
  return result;
}

export default function Todos({ loaderData }: Route.ComponentProps) {
  return <Form method="post">{/* form fields */}</Form>;
}
When to use:
  • Need to invalidate client caches before server mutation
  • Optimistic UI updates
  • Client-side validation before server call
在浏览器中处理数据变更,可选择调用服务端action:
typescript
import type { Route } from "./+types/todos";

export async function action({ request }: Route.ActionArgs) {
  // 服务端数据变更
  const formData = await request.formData();
  await db.createTodo({ title: formData.get("title") });
  return { success: true };
}

export async function clientAction({
  request,
  serverAction,
}: Route.ClientActionArgs) {
  // 先失效客户端缓存
  clientCache.invalidate();

  // 可选调用服务端action
  const result = await serverAction();
  return result;
}

export default function Todos({ loaderData }: Route.ComponentProps) {
  return <Form method="post">{/* 表单字段 */}</Form>;
}
何时使用
  • 需要在服务端数据变更前失效客户端缓存
  • 乐观UI更新
  • 服务端调用前的客户端验证

The
data()
Utility

data()
工具函数

Use
data()
to return responses with custom status codes and headers from loaders and actions:
typescript
import { data } from "react-router";
import type { Route } from "./+types/item";

// Return with custom status and headers
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const item = await createItem(formData);

  return data(item, {
    status: 201,
    headers: { "X-Custom-Header": "value" },
  });
}

// Throw 404 to trigger ErrorBoundary
export async function loader({ params }: Route.LoaderArgs) {
  const project = await db.getProject(params.id);

  if (!project) {
    throw data(null, { status: 404 });
  }

  return { project };
}
Common status codes:
  • 201
    - Resource created (after successful POST)
  • 400
    - Bad request (validation errors)
  • 404
    - Not found (missing resource)
  • 403
    - Forbidden (unauthorized access)
Thrown vs returned:
  • throw data(...)
    - Triggers
    ErrorBoundary
    , stops execution
  • return data(...)
    - Returns response, continues rendering
使用
data()
从loaders和actions返回带有自定义状态码和响应头的响应:
typescript
import { data } from "react-router";
import type { Route } from "./+types/item";

// 返回带有自定义状态码和响应头的响应
export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const item = await createItem(formData);

  return data(item, {
    status: 201,
    headers: { "X-Custom-Header": "value" },
  });
}

// 抛出404错误以触发ErrorBoundary
export async function loader({ params }: Route.LoaderArgs) {
  const project = await db.getProject(params.id);

  if (!project) {
    throw data(null, { status: 404 });
  }

  return { project };
}
常见状态码
  • 201
    - 资源创建成功(POST请求成功后)
  • 400
    - 错误请求(验证错误)
  • 404
    - 资源未找到
  • 403
    - 禁止访问(未授权)
抛出vs返回
  • throw data(...)
    - 触发
    ErrorBoundary
    ,停止执行
  • return data(...)
    - 返回响应,继续渲染

Route Module Exports

路由模块导出

Beyond
loader
,
action
, and the default component, route modules can export additional functions for metadata, headers, and revalidation control.
除了
loader
action
和默认组件外,路由模块还可以导出额外的函数用于元数据、响应头和重新验证控制。

meta

meta

Export page metadata (title, description, og tags):
typescript
import type { Route } from "./+types/product";

export function meta({ data }: Route.MetaArgs) {
  return [
    { title: data.product.name },
    { name: "description", content: data.product.description },
    { property: "og:title", content: data.product.name },
  ];
}
导出页面元数据(标题、描述、og标签):
typescript
import type { Route } from "./+types/product";

export function meta({ data }: Route.MetaArgs) {
  return [
    { title: data.product.name },
    { name: "description", content: data.product.description },
    { property: "og:title", content: data.product.name },
  ];
}

links

links

Export link tags (stylesheets, preloads, favicons):
typescript
import type { Route } from "./+types/product";

export function links() {
  return [
    { rel: "stylesheet", href: "/styles/product.css" },
    { rel: "preload", href: "/fonts/brand.woff2", as: "font", type: "font/woff2" },
  ];
}
导出link标签(样式表、预加载、图标):
typescript
import type { Route } from "./+types/product";

export function links() {
  return [
    { rel: "stylesheet", href: "/styles/product.css" },
    { rel: "preload", href: "/fonts/brand.woff2", as: "font", type: "font/woff2" },
  ];
}

headers

headers

Control HTTP response headers:
typescript
import type { Route } from "./+types/product";

export function headers({ loaderHeaders }: Route.HeadersArgs) {
  return {
    "Cache-Control": loaderHeaders.get("Cache-Control") ?? "max-age=300",
    "X-Custom-Header": "value",
  };
}
控制HTTP响应头:
typescript
import type { Route } from "./+types/product";

export function headers({ loaderHeaders }: Route.HeadersArgs) {
  return {
    "Cache-Control": loaderHeaders.get("Cache-Control") ?? "max-age=300",
    "X-Custom-Header": "value",
  };
}

shouldRevalidate

shouldRevalidate

Control when loaders re-run (optimize performance):
typescript
import type { Route } from "./+types/products";

export function shouldRevalidate({
  currentUrl,
  nextUrl,
  defaultShouldRevalidate,
}: Route.ShouldRevalidateArgs) {
  // Don't revalidate if only search params changed
  if (currentUrl.pathname === nextUrl.pathname) {
    return false;
  }

  return defaultShouldRevalidate;
}
Use cases for
shouldRevalidate
:
  • Skip revalidation when navigating within the same route
  • Prevent unnecessary refetches after certain actions
  • Optimize performance for expensive loaders
控制loaders何时重新运行(优化性能):
typescript
import type { Route } from "./+types/products";

export function shouldRevalidate({
  currentUrl,
  nextUrl,
  defaultShouldRevalidate,
}: Route.ShouldRevalidateArgs) {
  // 仅当搜索参数变化时不重新验证
  if (currentUrl.pathname === nextUrl.pathname) {
    return false;
  }

  return defaultShouldRevalidate;
}
shouldRevalidate
的使用场景
  • 在同一路由内导航时跳过重新验证
  • 特定action后避免不必要的数据重新获取
  • 对性能敏感的loaders进行优化

ErrorBoundary

ErrorBoundary

Handle errors that occur during loading or rendering:
typescript
import { isRouteErrorResponse, useRouteError } from "react-router";

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return <div>Something went wrong</div>;
}
处理加载或渲染期间发生的错误:
typescript
import { isRouteErrorResponse, useRouteError } from "react-router";

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return <div>出现未知错误</div>;
}

Server-Only Modules

仅服务端模块

Use
.server
and
.client
module conventions to prevent accidentally bundling server-only code (secrets, database clients) into the client bundle.
使用
.server
.client
模块约定,防止将仅服务端代码(密钥、数据库客户端)意外打包到客户端bundle中。

.server Modules

.server模块

Files ending in
.server.ts
or in a
.server/
directory are never bundled into the client:
app/
├── utils/
│   ├── db.server.ts      # Server-only: database client
│   ├── auth.server.ts    # Server-only: auth logic with secrets
│   └── format.ts         # Shared: safe for client and server
└── .server/
    └── secrets.ts        # Server-only: environment secrets
typescript
// app/utils/db.server.ts
import { PrismaClient } from "@prisma/client";

// This code never reaches the client bundle
export const db = new PrismaClient();
typescript
// app/routes/products.tsx
import { db } from "~/utils/db.server"; // Safe: only used in loader

export async function loader() {
  const products = await db.product.findMany();
  return { products };
}
.server.ts
结尾或位于
.server/
目录下的文件永远不会被打包到客户端
app/
├── utils/
│   ├── db.server.ts      // 仅服务端:数据库客户端
│   ├── auth.server.ts    // 仅服务端:包含密钥的认证逻辑
│   └── format.ts         // 共享:客户端和服务端均可安全使用
└── .server/
    └── secrets.ts        // 仅服务端:环境密钥
typescript
// app/utils/db.server.ts
import { PrismaClient } from "@prisma/client";

// 此代码永远不会到达客户端bundle
export const db = new PrismaClient();
typescript
// app/routes/products.tsx
import { db } from "~/utils/db.server"; // 安全:仅在loader中使用

export async function loader() {
  const products = await db.product.findMany();
  return { products };
}

.client Modules

.client模块

Files ending in
.client.ts
or in a
.client/
directory are never bundled into the server:
typescript
// app/utils/analytics.client.ts
// Browser-only code (window, document, etc.)
export function trackPageView(path: string) {
  window.gtag?.("event", "page_view", { page_path: path });
}
.client.ts
结尾或位于
.client/
目录下的文件永远不会被打包到服务端
typescript
// app/utils/analytics.client.ts
// 仅浏览器可用代码(window、document等)
export function trackPageView(path: string) {
  window.gtag?.("event", "page_view", { page_path: path });
}

Why This Matters

为什么这很重要

  • Security: Secrets and credentials stay on the server
  • Bundle size: Server-only code doesn't bloat client bundles
  • Compatibility: Browser-incompatible code (Node APIs, database clients) won't break the client build
Rule of thumb: If a module imports secrets, database clients, or Node-only APIs, name it
.server.ts
.
  • 安全性:密钥和凭证仅保留在服务端
  • Bundle大小:仅服务端代码不会增加客户端bundle体积
  • 兼容性:浏览器不兼容的代码(Node API、数据库客户端)不会破坏客户端构建
经验法则:如果模块导入了密钥、数据库客户端或仅Node可用的API,请将其命名为
.server.ts

Useful Utilities and Hooks

实用工具与钩子

useFetcher

useFetcher

Submit forms and load data without navigation:
typescript
import { useFetcher } from "react-router";

function TodoItem({ todo }) {
  const fetcher = useFetcher();
  const isDeleting = fetcher.state === "submitting";

  return (
    <div>
      <span>{todo.title}</span>
      <fetcher.Form method="post" action={`/todos/${todo.id}/delete`}>
        <button disabled={isDeleting}>
          {isDeleting ? "Deleting..." : "Delete"}
        </button>
      </fetcher.Form>
    </div>
  );
}
States:
"idle" | "submitting" | "loading"
Access data:
fetcher.data
(from loader/action) Methods:
fetcher.submit()
,
fetcher.load()
无需导航即可提交表单和加载数据:
typescript
import { useFetcher } from "react-router";

function TodoItem({ todo }) {
  const fetcher = useFetcher();
  const isDeleting = fetcher.state === "submitting";

  return (
    <div>
      <span>{todo.title}</span>
      <fetcher.Form method="post" action={`/todos/${todo.id}/delete`}>
        <button disabled={isDeleting}>
          {isDeleting ? "删除中..." : "删除"}
        </button>
      </fetcher.Form>
    </div>
  );
}
状态
"idle" | "submitting" | "loading"
访问数据
fetcher.data
(来自loader/action) 方法
fetcher.submit()
fetcher.load()

useNavigation

useNavigation

Track global navigation state:
typescript
import { useNavigation } from "react-router";

function GlobalLoadingIndicator() {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";

  return isNavigating ? <LoadingSpinner /> : null;
}
States:
"idle" | "loading" | "submitting"
跟踪全局导航状态:
typescript
import { useNavigation } from "react-router";

function GlobalLoadingIndicator() {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";

  return isNavigating ? <LoadingSpinner /> : null;
}
状态
"idle" | "loading" | "submitting"

useActionData

useActionData

Access data returned from the most recent action. In route modules, prefer
actionData
from props
(see "Accessing Loader/Action Data" above).
Use
useActionData
in deep child components:
typescript
import { useActionData, Form } from "react-router";

function LoginForm() {
  const actionData = useActionData<typeof import("../route").action>();

  return (
    <Form method="post">
      {actionData?.error && <div>{actionData.error}</div>}
      <input type="email" name="email" />
      <button type="submit">Login</button>
    </Form>
  );
}
Note:
actionData
is undefined until an action has been called
访问最近一次action返回的数据。在路由模块中,优先使用props中的
actionData
(请参阅上方“访问Loader/Action数据”部分)。
深层子组件中使用
useActionData
typescript
import { useActionData, Form } from "react-router";

function LoginForm() {
  const actionData = useActionData<typeof import("../route").action>();

  return (
    <Form method="post">
      {actionData?.error && <div>{actionData.error}</div>}
      <input type="email" name="email" />
      <button type="submit">登录</button>
    </Form>
  );
}
注意:在action被调用前,
actionData
为undefined

useLoaderData

useLoaderData

Access the current route's loader data. In route modules, prefer
loaderData
from props
(see "Accessing Loader/Action Data" above).
Use
useLoaderData
in deep child components that don't have direct access to route props:
typescript
import { useLoaderData } from "react-router";

// In a child component, not the route module default export
function ProductCard() {
  const { products } = useLoaderData<typeof import("../route").loader>();
  return <div>{products[0].name}</div>;
}
Important:
  • useLoaderData
    assumes the loader succeeded
  • Cannot be used in
    ErrorBoundary
    or
    Layout
    components — use
    useRouteLoaderData
    for those cases
  • Never use
    useLoaderData<Route.ComponentProps["loaderData"]>()
    — this is incorrect
访问当前路由的loader数据。在路由模块中,优先使用props中的
loaderData
(请参阅上方“访问Loader/Action数据”部分)。
无法直接访问路由props的深层子组件中使用
useLoaderData
typescript
import { useLoaderData } from "react-router";

// 在子组件中,而非路由模块的默认导出
function ProductCard() {
  const { products } = useLoaderData<typeof import("../route").loader>();
  return <div>{products[0].name}</div>;
}
重要提示
  • useLoaderData
    假设loader执行成功
  • 不能在
    ErrorBoundary
    Layout
    组件中使用——这些场景请使用
    useRouteLoaderData
  • 切勿使用
    useLoaderData<Route.ComponentProps["loaderData"]>()
    ——这是错误的

useRouteLoaderData

useRouteLoaderData

Access loader data from parent or sibling routes by route ID. Essential for
ErrorBoundary
and
Layout
components where
useLoaderData
is not allowed.
Type-safe pattern with
typeof
:
typescript
import { useRouteLoaderData } from "react-router";
import type { loader as rootLoader } from "./root";

export function Layout({ children }) {
  // Type-safe: infers types from root loader
  const rootData = useRouteLoaderData<typeof rootLoader>("root");

  // Always check for undefined (loader may have thrown)
  if (rootData?.user) {
    return <div>Welcome, {rootData.user.name}</div>;
  }

  return <div>Not authenticated</div>;
}
Basic usage (untyped):
typescript
import { useRouteLoaderData } from "react-router";

export default function ChildComponent() {
  const rootData = useRouteLoaderData("root");

  if (rootData?.user) {
    return <div>Welcome, {rootData.user.name}</div>;
  }

  return <div>Not authenticated</div>;
}
When to use:
  • Accessing parent route data (e.g., user auth from root loader)
  • Sharing data across route hierarchy
  • In
    ErrorBoundary
    or
    Layout
    components where
    useLoaderData
    is not allowed
Route IDs: Automatically generated from file paths:
  • app/root.tsx
    "root"
  • app/routes/products.tsx
    "routes/products"
  • app/routes/products.$id.tsx
    "routes/products.$id"
You can also specify custom IDs in
routes.ts
:
typescript
import { route } from "@react-router/dev/routes";

export default [
  route("/products/:id", "./product.tsx", { id: "product-detail" }),
];
通过路由ID访问父路由或兄弟路由的loader数据。在
ErrorBoundary
Layout
组件中是必需的,因为这些场景不允许使用
useLoaderData
结合
typeof
的类型安全模式
typescript
import { useRouteLoaderData } from "react-router";
import type { loader as rootLoader } from "./root";

export function Layout({ children }) {
  // 类型安全:从root loader推断类型
  const rootData = useRouteLoaderData<typeof rootLoader>("root");

  // 始终检查是否为undefined(loader可能抛出了错误)
  if (rootData?.user) {
    return <div>欢迎,{rootData.user.name}</div>;
  }

  return <div>未登录</div>;
}
基础用法(无类型)
typescript
import { useRouteLoaderData } from "react-router";

export default function ChildComponent() {
  const rootData = useRouteLoaderData("root");

  if (rootData?.user) {
    return <div>欢迎,{rootData.user.name}</div>;
  }

  return <div>未登录</div>;
}
何时使用
  • 访问父路由数据(例如来自root loader的用户认证信息)
  • 在路由层级间共享数据
  • ErrorBoundary
    Layout
    组件中使用(
    useLoaderData
    不允许的场景)
路由ID:从文件路径自动生成:
  • app/root.tsx
    "root"
  • app/routes/products.tsx
    "routes/products"
  • app/routes/products.$id.tsx
    "routes/products.$id"
你也可以在
routes.ts
中指定自定义ID:
typescript
import { route } from "@react-router/dev/routes";

export default [
  route("/products/:id", "./product.tsx", { id: "product-detail" }),
];

useMatches

useMatches

Access all matched routes and their data/handles:
typescript
import { useMatches } from "react-router";

export function Layout({ children }) {
  const matches = useMatches();

  // Access all matched routes
  matches.forEach((match) => {
    console.log(match.id);           // Route ID
    console.log(match.pathname);     // URL pathname
    console.log(match.params);       // URL params
    console.log(match.loaderData);   // Loader data (may be undefined)
    console.log(match.handle);       // Custom handle metadata
  });

  return <div>{children}</div>;
}
Note: Use
match.loaderData
instead of
match.data
. The
data
property is deprecated.
Common use cases:
  • Building breadcrumbs from route hierarchy
  • Creating dynamic navigation based on current route
  • Accessing metadata from all matched routes
Type safety with UIMatch:
typescript
import { useMatches, type UIMatch } from "react-router";

const matches = useMatches();
const rootMatch = matches[0] as UIMatch<{ user: User } | undefined>;

// Guard against undefined loaderData (loader may have thrown)
if (rootMatch.loaderData?.user) {
  const { user } = rootMatch.loaderData;
}
访问所有匹配的路由及其数据/handles:
typescript
import { useMatches } from "react-router";

export function Layout({ children }) {
  const matches = useMatches();

  // 访问所有匹配的路由
  matches.forEach((match) => {
    console.log(match.id);           // 路由ID
    console.log(match.pathname);     // URL路径
    console.log(match.params);       // URL参数
    console.log(match.loaderData);   // Loader数据(可能为undefined)
    console.log(match.handle);       // 自定义handle元数据
  });

  return <div>{children}</div>;
}
注意:使用
match.loaderData
而非
match.data
data
属性已被废弃。
常见使用场景
  • 从路由层级构建面包屑
  • 根据当前路由创建动态导航
  • 访问所有匹配路由的元数据
结合UIMatch的类型安全
typescript
import { useMatches, type UIMatch } from "react-router";

const matches = useMatches();
const rootMatch = matches[0] as UIMatch<{ user: User } | undefined>;

// 防范loaderData为undefined的情况(loader可能抛出了错误)
if (rootMatch.loaderData?.user) {
  const { user } = rootMatch.loaderData;
}

Form Component

Form组件

Use React Router's
Form
for enhanced form handling:
typescript
import { Form } from "react-router";

<Form method="post" action="/todos">
  <input name="title" />
  <button type="submit">Create</button>
</Form>

// With navigate={false} to prevent navigation after action
<Form method="post" navigate={false}>
  {/* ... */}
</Form>
See references/forms.md for form validation patterns, optimistic UI, and pending states.
使用React Router的
Form
组件增强表单处理:
typescript
import { Form } from "react-router";

<Form method="post" action="/todos">
  <input name="title" />
  <button type="submit">创建</button>
</Form>

// 使用navigate={false}以在action完成后阻止导航
<Form method="post" navigate={false}>
  {/* ... */}
</Form>
有关表单验证模式、乐观UI和pending状态,请参阅references/forms.md

useParams

useParams

Access route parameters in components:
typescript
import { useParams } from "react-router";

function ProductDetail() {
  const { productId } = useParams();
  return <div>Product: {productId}</div>;
}
Note: In route modules, prefer accessing params from
Route.ComponentProps
or
Route.LoaderArgs
.
在组件中访问路由参数:
typescript
import { useParams } from "react-router";

function ProductDetail() {
  const { productId } = useParams();
  return <div>产品:{productId}</div>;
}
注意:在路由模块中,优先从
Route.ComponentProps
Route.LoaderArgs
中访问params。

useRevalidator

useRevalidator

Manually trigger data revalidation:
typescript
import { useRevalidator } from "react-router";

function RefreshButton() {
  const revalidator = useRevalidator();

  return (
    <button
      onClick={() => revalidator.revalidate()}
      disabled={revalidator.state === "loading"}
    >
      {revalidator.state === "loading" ? "Refreshing..." : "Refresh"}
    </button>
  );
}
Use cases: Polling, window focus refresh, manual refresh buttons.
手动触发数据重新验证:
typescript
import { useRevalidator } from "react-router";

function RefreshButton() {
  const revalidator = useRevalidator();

  return (
    <button
      onClick={() => revalidator.revalidate()}
      disabled={revalidator.state === "loading"}
    >
      {revalidator.state === "loading" ? "刷新中..." : "刷新"}
    </button>
  );
}
使用场景:轮询、窗口聚焦时刷新、手动刷新按钮。

useNavigate

useNavigate

Programmatic navigation without user interaction:
typescript
import { useNavigate } from "react-router";

function LogoutButton() {
  const navigate = useNavigate();

  const handleLogout = async () => {
    await logout();
    navigate("/login", { replace: true });
  };

  return <button onClick={handleLogout}>Logout</button>;
}
See references/navigation.md for navigation options, Outlet, and redirect patterns.
无需用户交互的编程式导航:
typescript
import { useNavigate } from "react-router";

function LogoutButton() {
  const navigate = useNavigate();

  const handleLogout = async () => {
    await logout();
    navigate("/login", { replace: true });
  };

  return <button onClick={handleLogout}>退出登录</button>;
}
有关导航选项、Outlet和重定向模式,请参阅references/navigation.md

URL Search Params

URL搜索参数

For filters, pagination, search, and shareable UI state.
Quick example:
typescript
import { useSearchParams } from "react-router";

export default function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get("category") || "all";

  const handleCategoryChange = (newCategory: string) => {
    setSearchParams((prev) => {
      prev.set("category", newCategory);
      prev.set("page", "1"); // Reset page when filter changes
      return prev;
    });
  };

  return (/* ... */);
}
In loaders:
typescript
export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const category = url.searchParams.get("category") || "all";
  const products = await db.getProducts({ category });
  return { products, category };
}
See references/search-params.md for pagination patterns, filtering with forms, type-safe parsing, and debounced search.
用于筛选、分页、搜索和可共享的UI状态。
快速示例:
typescript
import { useSearchParams } from "react-router";

export default function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get("category") || "all";

  const handleCategoryChange = (newCategory: string) => {
    setSearchParams((prev) => {
      prev.set("category", newCategory);
      prev.set("page", "1"); // 筛选条件变化时重置页码
      return prev;
    });
  };

  return (/* ... */);
}
在loaders中使用:
typescript
export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const category = url.searchParams.get("category") || "all";
  const products = await db.getProducts({ category });
  return { products, category };
}
有关分页模式、结合表单的筛选、类型安全解析和防抖搜索,请参阅references/search-params.md

Route Metadata with handle

结合handle的路由元数据

Export a
handle
object to attach custom metadata to routes. This metadata is accessible via
useMatches()
in ancestor components.
导出
handle
对象为路由附加自定义元数据。该元数据可通过祖先组件中的
useMatches()
访问。

Basic handle Export

基础handle导出

typescript
// app/routes/products.tsx
import { Link } from "react-router";

export const handle = {
  breadcrumb: () => <Link to="/products">Products</Link>,
  title: "Products",
  icon: "📦",
};
typescript
// app/routes/products.tsx
import { Link } from "react-router";

export const handle = {
  breadcrumb: () => <Link to="/products">产品</Link>,
  title: "产品",
  icon: "📦",
};

Dynamic Breadcrumbs Pattern

动态面包屑模式

Use
handle
with
useMatches
to build breadcrumbs:
typescript
// app/routes/products.$id.tsx
import type { Route } from "./+types/products.$id";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await db.getProduct(params.id);
  return { product };
}

export const handle = {
  breadcrumb: (match: any) => (
    <Link to={`/products/${match.params.id}`}>
      {match.loaderData?.product?.name || "Product"}
    </Link>
  ),
};

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}
结合
handle
useMatches
构建面包屑:
typescript
// app/routes/products.$id.tsx
import type { Route } from "./+types/products.$id";

export async function loader({ params }: Route.LoaderArgs) {
  const product = await db.getProduct(params.id);
  return { product };
}

export const handle = {
  breadcrumb: (match: any) => (
    <Link to={`/products/${match.params.id}`}>
      {match.loaderData?.product?.name || "产品"}
    </Link>
  ),
};

export default function Product({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.product.name}</div>;
}

Rendering Breadcrumbs in Layout

在Layout中渲染面包屑

typescript
// app/root.tsx
import { useMatches, Outlet } from "react-router";

export function Layout({ children }) {
  const matches = useMatches();

  return (
    <html>
      <body>
        <nav>
          <ol>
            {matches
              .filter((match) => match.handle?.breadcrumb)
              .map((match, index) => (
                <li key={index}>
                  {match.handle.breadcrumb(match)}
                </li>
              ))}
          </ol>
        </nav>
        {children}
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
Common handle use cases:
  • Breadcrumb navigation
  • Page titles and metadata
  • Icons for navigation items
  • Role-based access control metadata
  • Analytics tracking data
typescript
// app/root.tsx
import { useMatches, Outlet } from "react-router";

export function Layout({ children }) {
  const matches = useMatches();

  return (
    <html>
      <body>
        <nav>
          <ol>
            {matches
              .filter((match) => match.handle?.breadcrumb)
              .map((match, index) => (
                <li key={index}>
                  {match.handle.breadcrumb(match)}
                </li>
              ))}
          </ol>
        </nav>
        {children}
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
handle的常见使用场景
  • 面包屑导航
  • 页面标题和元数据
  • 导航项图标
  • 基于角色的访问控制元数据
  • 分析跟踪数据

Middleware (v7.9.0+)

中间件(v7.9.0+)

Middleware runs code before/after route handlers for authentication, logging, context sharing.
Quick example:
typescript
// app/middleware/auth.ts
import { redirect, createContext } from "react-router";

export const userContext = createContext<User>();

export async function authMiddleware({ request, context }) {
  const session = await getSession(request);
  if (!session.get("userId")) throw redirect("/login");

  const user = await getUserById(session.get("userId"));
  context.set(userContext, user);
}

// app/routes/dashboard.tsx
export const middleware = [authMiddleware] satisfies Route.MiddlewareFunction[];

export async function loader({ context }: Route.LoaderArgs) {
  const user = context.get(userContext);
  return { user };
}
Enable with
future.v8_middleware: true
in
react-router.config.ts
.
See references/middleware.md for execution order, error handling, and role-based access patterns.
中间件在路由处理器之前/之后运行代码,用于认证、日志、上下文共享等场景。
快速示例:
typescript
// app/middleware/auth.ts
import { redirect, createContext } from "react-router";

export const userContext = createContext<User>();

export async function authMiddleware({ request, context }) {
  const session = await getSession(request);
  if (!session.get("userId")) throw redirect("/login");

  const user = await getUserById(session.get("userId"));
  context.set(userContext, user);
}

// app/routes/dashboard.tsx
export const middleware = [authMiddleware] satisfies Route.MiddlewareFunction[];

export async function loader({ context }: Route.LoaderArgs) {
  const user = context.get(userContext);
  return { user };
}
需要在
react-router.config.ts
中启用
future.v8_middleware: true
有关执行顺序、错误处理和基于角色的访问模式,请参阅references/middleware.md

SSR and Pre-rendering

SSR与预渲染

Configure SSR

配置SSR

Enable server-side rendering in
react-router.config.ts
:
typescript
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
} satisfies Config;
react-router.config.ts
中启用服务端渲染:
typescript
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
} satisfies Config;

Configure Pre-rendering

配置预渲染

Generate static HTML at build time:
typescript
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true, // Can be true or false
  async prerender() {
    return ["/", "/about", "/products", "/contact"];
  },
} satisfies Config;
Static-only mode (no runtime server):
typescript
export default {
  ssr: false,
  prerender: true, // Pre-renders all static routes
} satisfies Config;
在构建时生成静态HTML:
typescript
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true, // 可以是true或false
  async prerender() {
    return ["/", "/about", "/products", "/contact"];
  },
} satisfies Config;
仅静态模式(无运行时服务端):
typescript
export default {
  ssr: false,
  prerender: true, // 预渲染所有静态路由
} satisfies Config;

Async Streaming with Promises

结合Promises的异步流式渲染

Stream non-critical data while rendering critical data immediately.
defer()
is deprecated - just return promises directly.
Quick example:
typescript
export async function loader() {
  const user = await db.getUser(); // Critical - await
  const stats = db.getStats();     // Non-critical - don't await

  return { user, stats };
}

export default function Dashboard({ loaderData }: Route.ComponentProps) {
  const { user, stats } = loaderData;

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Await resolve={stats}>
          {(resolvedStats) => <StatsCard data={resolvedStats} />}
        </Await>
      </Suspense>
    </div>
  );
}
Important: Promises must be wrapped in an object (
return { reviews }
not
return reviews
).
See references/streaming.md for error handling, useAsyncValue patterns, and when to use streaming.
在立即渲染关键数据的同时,流式传输非关键数据。
defer()
已被废弃——直接返回promises即可。
快速示例:
typescript
export async function loader() {
  const user = await db.getUser(); // 关键数据 - 等待返回
  const stats = db.getStats();     // 非关键数据 - 不等待

  return { user, stats };
}

export default function Dashboard({ loaderData }: Route.ComponentProps) {
  const { user, stats } = loaderData;

  return (
    <div>
      <h1>欢迎,{user.name}</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Await resolve={stats}>
          {(resolvedStats) => <StatsCard data={resolvedStats} />}
        </Await>
      </Suspense>
    </div>
  );
}
重要提示:Promises必须包裹在对象中(
return { reviews }
而非
return reviews
)。
有关错误处理、useAsyncValue模式和何时使用流式渲染,请参阅references/streaming.md

Common Patterns

常见模式

Loading States with HydrateFallback

结合HydrateFallback的加载状态

Show loading UI during initial hydration when
clientLoader.hydrate = true
:
typescript
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
  const data = await serverLoader();
  return data;
}
clientLoader.hydrate = true as const;

export function HydrateFallback() {
  return <div>Loading...</div>;
}

export default function Component({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.content}</div>;
}
Important:
HydrateFallback
cannot render
<Outlet />
as child routes may not be ready.
clientLoader.hydrate = true
时,在初始hydration期间显示加载UI:
typescript
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
  const data = await serverLoader();
  return data;
}
clientLoader.hydrate = true as const;

export function HydrateFallback() {
  return <div>加载中...</div>;
}

export default function Component({ loaderData }: Route.ComponentProps) {
  return <div>{loaderData.content}</div>;
}
重要提示
HydrateFallback
不能渲染
<Outlet />
,因为子路由可能尚未准备好。

Error Handling

错误处理

Use error boundaries for loader/action errors:
typescript
import { isRouteErrorResponse, useRouteError } from "react-router";

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return <div>Something went wrong!</div>;
}
使用错误边界处理loader/action错误:
typescript
import { isRouteErrorResponse, useRouteError } from "react-router";

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return <div>出现未知错误!</div>;
}

Pending UI with useNavigation

结合useNavigation的Pending UI

Show pending states during navigation:
typescript
import { useNavigation } from "react-router";

export default function Products({ loaderData }: Route.ComponentProps) {
  const navigation = useNavigation();
  const isLoading = navigation.state === "loading";

  return (
    <div className={isLoading ? "opacity-50" : ""}>
      {loaderData.products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
导航期间显示pending状态:
typescript
import { useNavigation } from "react-router";

export default function Products({ loaderData }: Route.ComponentProps) {
  const navigation = useNavigation();
  const isLoading = navigation.state === "loading";

  return (
    <div className={isLoading ? "opacity-50" : ""}>
      {loaderData.products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Resource Routes (API Endpoints)

资源路由(API端点)

Resource routes serve non-UI responses (JSON, PDF, webhooks). A route is a resource route when it exports
loader
/
action
but no default component:
typescript
// app/routes/api.users.tsx
export async function loader() {
  const users = await db.getUsers();
  return Response.json(users);
}

export async function action({ request }: Route.ActionArgs) {
  const data = await request.json();
  const user = await db.createUser(data);
  return Response.json(user, { status: 201 });
}
// No default export = resource route
Link with
reloadDocument
to prevent client-side routing:
typescript
<Link reloadDocument to="/api/report.pdf">Download PDF</Link>
See references/resource-routes.md for HTTP method handling, file serving, and webhook patterns.
资源路由用于提供非UI响应(JSON、PDF、Webhooks)。当路由导出
loader
/
action
无默认组件时,即为资源路由:
typescript
// app/routes/api.users.tsx
export async function loader() {
  const users = await db.getUsers();
  return Response.json(users);
}

export async function action({ request }: Route.ActionArgs) {
  const data = await request.json();
  const user = await db.createUser(data);
  return Response.json(user, { status: 201 });
}
// 无默认导出 = 资源路由
结合
reloadDocument
链接以阻止客户端路由:
typescript
<Link reloadDocument to="/api/report.pdf">下载PDF</Link>
有关HTTP方法处理、文件服务和Webhook模式,请参阅references/resource-routes.md

Decision Tree

决策树

Do you need to access client-only APIs (localStorage, browser state)?
├─ YES → Use clientLoader (no server loader)
└─ NO  → Continue

Do you need to combine server and client data?
├─ YES → Use loader + clientLoader with hydrate = true
└─ NO  → Continue

Do you want to cache data on the client?
├─ YES → Use loader + clientLoader with caching logic + hydrate = true
└─ NO  → Continue

Do you want to skip server on subsequent navigations?
├─ YES → Use loader + clientLoader (BFF pattern, no hydrate)
└─ NO  → Continue

Do you have slow/non-critical data that blocks rendering?
├─ YES → Return promises without awaiting - wrap with Suspense/Await in component
└─ NO  → Use server-only loader (PREFERRED)
是否需要访问仅客户端可用的API(localStorage、浏览器状态)?
├─ 是 → 使用clientLoader(无服务端loader)
└─ 否 → 继续

是否需要结合服务端和客户端数据?
├─ 是 → 使用loader + clientLoader并设置hydrate = true
└─ 否 → 继续

是否需要在客户端缓存数据?
├─ 是 → 使用loader + 带有缓存逻辑的clientLoader并设置hydrate = true
└─ 否 → 继续

是否希望在后续导航时跳过服务端?
├─ 是 → 使用loader + clientLoader(BFF模式,不设置hydrate)
└─ 否 → 继续

是否存在阻塞渲染的慢/非关键数据?
├─ 是 → 返回promises但不等待 - 在组件中结合Suspense/Await包裹
└─ 否 → 使用仅服务端loader(推荐)

Checklist

检查清单

Before completing any React Router 7 implementation:
  • All route modules use
    Route.*
    types from
    ./+types/<route>
  • Data fetching prioritizes server-side loaders
  • clientLoader.hydrate = true as const
    is set correctly when needed
  • HydrateFallback
    is exported when
    clientLoader.hydrate = true
  • Actions use server-side mutations with automatic revalidation
  • Forms use
    <Form>
    component from react-router, not native
    <form>
  • Type-safe
    href()
    utility is used for route generation
  • Error boundaries are implemented for route errors
  • Loading states use
    useNavigation
    or fetcher states
  • No client-side data fetching unless absolutely necessary
  • Slow/non-critical data returned as promises (not awaited) for streaming
  • Critical data is awaited, non-critical data is streamed
  • <Await>
    wrapped in
    <Suspense>
    with fallback UI
  • Error handling implemented for streaming promises (
    errorElement
    or
    useAsyncError
    )
  • Search params used for shareable UI state (filters, pagination, search)
  • Search params validated and parsed in loaders with proper defaults
  • Search param values returned from loader and used as
    defaultValue
    in forms
  • <Form method="get">
    used for filter forms (not POST)
  • Use
    useRouteLoaderData
    instead of
    useLoaderData
    in ErrorBoundary/Layout components
  • Parent route data accessed via
    useRouteLoaderData("route-id")
    with proper undefined checks
  • handle
    exports used for route metadata (breadcrumbs, titles, etc.)
  • Middleware used for authentication/authorization instead of loader-only patterns
  • Context API (
    context.set/get
    ) used for sharing data between middleware and loaders
  • data()
    utility used for custom status codes (404, 201, etc.)
  • Route IDs understood for
    useRouteLoaderData
    calls
  • meta
    ,
    links
    ,
    headers
    exports used where appropriate
  • shouldRevalidate
    considered for performance-critical loaders
  • Server-only code uses
    .server.ts
    naming convention
  • Secrets and database clients never imported in client-accessible modules
  • Routes configured in
    routes.ts
    with appropriate helpers (
    route
    ,
    index
    ,
    layout
    ,
    prefix
    )
  • Resource routes (API endpoints) export no default component
  • Form validation returns errors with
    data({ errors }, { status: 400 })
  • Optimistic UI uses
    fetcher.formData
    for immediate feedback
完成任何React Router 7实现前,请检查以下项:
  • 所有路由模块使用
    ./+types/<route>
    中的
    Route.*
    类型
  • 数据获取优先使用服务端loaders
  • clientLoader.hydrate = true as const
    在需要时正确设置
  • clientLoader.hydrate = true
    时导出
    HydrateFallback
  • Actions使用服务端数据变更并自动重新验证
  • 表单使用react-router的
    <Form>
    组件,而非原生
    <form>
  • 类型安全的
    href()
    工具函数用于路由生成
  • 为路由错误实现了错误边界
  • 加载状态使用
    useNavigation
    或fetcher状态
  • 除非绝对必要,否则不使用客户端数据获取
  • 慢/非关键数据以promises形式返回(不等待)以进行流式渲染
  • 关键数据等待返回,非关键数据流式传输
  • <Await>
    包裹在带有fallback UI的
    <Suspense>
  • 为流式promises实现了错误处理(
    errorElement
    useAsyncError
  • 搜索参数用于可共享的UI状态(筛选、分页、搜索)
  • 搜索参数在loaders中验证并解析,带有合适的默认值
  • 搜索参数值从loader返回并在表单中用作
    defaultValue
  • 筛选表单使用
    <Form method="get">
    (而非POST)
  • 在ErrorBoundary/Layout组件中使用
    useRouteLoaderData
    而非
    useLoaderData
  • 父路由数据通过
    useRouteLoaderData("route-id")
    访问,并带有正确的undefined检查
  • handle
    导出用于路由元数据(面包屑、标题等)
  • 中间件用于认证/授权,而非仅loader模式
  • 上下文API(
    context.set/get
    )用于在中间件和loaders之间共享数据
  • data()
    工具函数用于自定义状态码(404、201等)
  • 理解用于
    useRouteLoaderData
    调用的路由ID
  • 合适的场景使用
    meta
    links
    headers
    导出
  • 对性能敏感的loaders考虑使用
    shouldRevalidate
  • 仅服务端代码使用
    .server.ts
    命名约定
  • 密钥和数据库客户端从不导入到客户端可访问的模块中
  • 路由在
    routes.ts
    中使用合适的辅助函数(
    route
    index
    layout
    prefix
    )配置
  • 资源路由(API端点)无默认导出
  • 表单验证通过
    data({ errors }, { status: 400 })
    返回错误
  • 乐观UI使用
    fetcher.formData
    实现即时反馈

Bundled References

内置参考文档

  • routes-config.md - Route configuration, helpers, Outlet, splats
  • navigation.md - Redirects, useNavigate, Outlet context
  • forms.md - Validation, optimistic UI, pending states
  • resource-routes.md - API endpoints, webhooks, file serving
  • middleware.md - Authentication, context API, execution order
  • search-params.md - Pagination, filtering, type-safe parsing
  • streaming.md - Suspense, Await, deferred data
  • routes-config.md - 路由配置、辅助函数、Outlet、通配符路由
  • navigation.md - 重定向、useNavigate、Outlet上下文
  • forms.md - 验证、乐观UI、pending状态
  • resource-routes.md - API端点、Webhooks、文件服务
  • middleware.md - 认证、上下文API、执行顺序
  • search-params.md - 分页、筛选、类型安全解析
  • streaming.md - Suspense、Await、延迟数据

External References

外部参考文档

Key Notes

关键注意事项

  • Always prefer server-side data loading over client-side
  • In route modules, use props (
    Route.ComponentProps
    ) — hooks are for deep child components
  • Never use
    useLoaderData<Route.ComponentProps["loaderData"]>()
    — this is incorrect
  • Use
    as const
    when setting
    clientLoader.hydrate = true
  • HydrateFallback
    is required when
    clientLoader.hydrate = true
  • Return promises without awaiting to stream slow/non-critical data
  • Always wrap
    <Await>
    in
    <Suspense>
    with fallback UI
  • useLoaderData
    cannot be used in
    ErrorBoundary
    or
    Layout
    — use
    useRouteLoaderData
  • Use
    .server.ts
    naming for modules containing secrets or database clients
  • Middleware requires
    future.v8_middleware: true
    flag (v7.9.0+)
  • 始终优先使用服务端数据加载而非客户端
  • 在路由模块中,使用props
    Route.ComponentProps
    )——钩子适用于深层子组件
  • 切勿使用
    useLoaderData<Route.ComponentProps["loaderData"]>()
    ——这是错误的
  • 设置
    clientLoader.hydrate = true
    时使用
    as const
  • clientLoader.hydrate = true
    时需要
    HydrateFallback
  • 返回promises但不等待以流式传输慢/非关键数据
  • 始终将
    <Await>
    包裹在带有fallback UI的
    <Suspense>
  • useLoaderData
    不能在
    ErrorBoundary
    Layout
    中使用——请使用
    useRouteLoaderData
  • 包含密钥或数据库客户端的模块使用
    .server.ts
    命名
  • 中间件需要
    future.v8_middleware: true
    标志(v7.9.0+) ",