react-router-7-framework
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact 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:
| Version | Features |
|---|---|
| v7.0 | Framework mode, type generation, loaders/actions, |
| v7.5 | |
| v7.9+ | Stable middleware and context APIs, v8 future flags |
本指南针对框架模式下的**React Router 7.9.0+**版本。各版本关键特性:
| 版本 | 特性 |
|---|---|
| v7.0 | 框架模式、类型生成、loaders/actions、 |
| v7.5 | 用于类型安全链接的 |
| v7.9+ | 稳定的中间件和上下文API、v8未来特性标志 |
Future Flags
未来特性标志
Enable v8 features in :
react-router.config.tstypescript
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.
在中启用v8特性:
react-router.config.tstypescript
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_middleware: true, // 中间件支持
v8_splitRouteModules: true, // 路由模块拆分以提升性能
},
} satisfies Config;这些特性将在v8版本中成为默认配置。
Core Principles
核心原则
- Server-First: Fetch and process data on the server whenever possible. Only use client-side data fetching when absolutely necessary.
- Maximum Type Safety: Always use React Router 7's generated types (,
Route.LoaderArgs, etc.). Access loader/action data from props or useRoute.ComponentProps/typeof loaderwith hooks.typeof action - Proper Hydration: Understand when to use and when to skip it
clientLoader.hydrate = true - Declarative Data: Colocate data requirements with routes using loaders
- Progressive Enhancement: Use actions for mutations with automatic revalidation
- 服务端优先:尽可能在服务端获取和处理数据。仅在绝对必要时使用客户端数据获取。
- 最大化类型安全:始终使用React Router 7生成的类型(、
Route.LoaderArgs等)。通过props或结合钩子使用Route.ComponentProps/typeof loader来访问loader/action的数据。typeof action - 合适的Hydration:了解何时使用以及何时跳过它
clientLoader.hydrate = true - 声明式数据:使用loaders将数据需求与路由进行内聚管理
- 渐进式增强:使用actions处理变更并自动重新验证数据
Route Configuration
路由配置
Define routes in using helper functions:
app/routes.tstypescript
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.tstypescript
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 for each route. Always import and use these types:
.react-router/types/+types/<route-file>.d.tstypescript
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.tstypescript
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
可用的路由类型
- - Types for loader parameters (params, request, context)
Route.LoaderArgs - - Types for action parameters
Route.ActionArgs - - Types for clientLoader parameters (includes serverLoader)
Route.ClientLoaderArgs - - Types for clientAction parameters (includes serverAction)
Route.ClientActionArgs - - Types for component props (includes loaderData, actionData, matches, etc.)
Route.ComponentProps
- - loader参数的类型(params、request、context)
Route.LoaderArgs - - action参数的类型
Route.ActionArgs - - clientLoader参数的类型(包含serverLoader)
Route.ClientLoaderArgs - - clientAction参数的类型(包含serverAction)
Route.ClientActionArgs - - 组件props的类型(包含loaderData、actionData、matches等)
Route.ComponentProps
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 (, ) are for non-route-module contexts — deep child components, shared UI, or when testing:
useLoaderDatauseActionDatatypescript
// 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 likeexist largely for migration from Remix and are considered secondary to the props pattern. TheuseLoaderData<typeof loader>()types via props are the "most type-safe / least foot-gun" approach.Route.*
❌ Never use: — this pattern is incorrect.
useLoaderData<Route.ComponentProps["loaderData"]>()在路由模块的默认导出中,始终使用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>;
}何时改用钩子:
钩子(、)适用于非路由模块上下文——深层子组件、共享UI或测试场景:
useLoaderDatauseActionDatatypescript
// 在无法直接访问路由props的子组件中
import { useLoaderData } from "react-router";
function ProductDetails() {
// 使用typeof进行类型推断
const { product } = useLoaderData<typeof import("./route").loader>();
return <span>{product.description}</span>;
}注意:像这样的钩子泛型主要用于从Remix迁移的场景,相对于props模式是次要选择。通过props使用useLoaderData<typeof loader>()类型是“最类型安全/最少陷阱”的方式。Route.*
❌ 切勿使用:——这种模式是错误的。
useLoaderData<Route.ComponentProps["loaderData"]>()Type-Safe Links
类型安全链接
Use the utility for type-safe route generation (v7.5+):
hreftypescript
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 paramBenefits:
- Compile-time validation of route paths
- Required params are enforced
- Refactoring routes updates all usages
- IDE autocomplete for available routes
使用工具函数生成类型安全的路由链接(v7.5+):
hreftypescript
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: is implicit when no server exists.
clientLoader.hydrate = trueloader仅在浏览器中加载数据:
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)
- 客户端缓存策略
- 无服务端环境可用
重要提示:当不存在服务端时,是隐式生效的。
loaderclientLoader.hydrate = true3. 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 to call clientLoader during initial hydration.
clientLoader.hydrate = true as const用客户端数据增强服务端数据:
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>;
}何时使用:
- 结合服务端数据与仅客户端可用数据(用户偏好、客户端状态)
- 用缓存数据增强服务端数据
重要提示:设置以在初始hydration期间调用clientLoader。
clientLoader.hydrate = true as const4. 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 for this pattern. You want clientLoader to skip during hydration.
clientLoader.hydrate = true初始请求时加载服务端数据,后续直接调用客户端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在hydration期间跳过执行。
clientLoader.hydrate = true5. 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>, anduseFetcheruseSubmit
在服务端处理数据变更,自动重新验证数据:
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>和useFetcheruseSubmit
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()data()
工具函数
data()Use to return responses with custom status codes and headers from loaders and actions:
data()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:
- - Resource created (after successful POST)
201 - - Bad request (validation errors)
400 - - Not found (missing resource)
404 - - Forbidden (unauthorized access)
403
Thrown vs returned:
- - Triggers
throw data(...), stops executionErrorBoundary - - Returns response, continues rendering
return data(...)
使用从loaders和actions返回带有自定义状态码和响应头的响应:
data()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 };
}常见状态码:
- - 资源创建成功(POST请求成功后)
201 - - 错误请求(验证错误)
400 - - 资源未找到
404 - - 禁止访问(未授权)
403
抛出vs返回:
- - 触发
throw data(...),停止执行ErrorBoundary - - 返回响应,继续渲染
return data(...)
Route Module Exports
路由模块导出
Beyond , , and the default component, route modules can export additional functions for metadata, headers, and revalidation control.
loaderaction除了、和默认组件外,路由模块还可以导出额外的函数用于元数据、响应头和重新验证控制。
loaderactionmeta
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 and module conventions to prevent accidentally bundling server-only code (secrets, database clients) into the client bundle.
.server.client使用和模块约定,防止将仅服务端代码(密钥、数据库客户端)意外打包到客户端bundle中。
.server.client.server Modules
.server模块
Files ending in or in a directory are never bundled into the client:
.server.ts.server/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 secretstypescript
// 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 or in a directory are never bundled into the server:
.client.ts.client/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:
Access data: (from loader/action)
Methods: ,
"idle" | "submitting" | "loading"fetcher.datafetcher.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>
);
}状态:
访问数据:(来自loader/action)
方法:、
"idle" | "submitting" | "loading"fetcher.datafetcher.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 from props (see "Accessing Loader/Action Data" above).
actionDataUse in deep child components:
useActionDatatypescript
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: is undefined until an action has been called
actionData访问最近一次action返回的数据。在路由模块中,优先使用props中的(请参阅上方“访问Loader/Action数据”部分)。
actionData在深层子组件中使用:
useActionDatatypescript
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被调用前,为undefined
actionDatauseLoaderData
useLoaderData
Access the current route's loader data. In route modules, prefer from props (see "Accessing Loader/Action Data" above).
loaderDataUse in deep child components that don't have direct access to route props:
useLoaderDatatypescript
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:
- assumes the loader succeeded
useLoaderData - Cannot be used in or
ErrorBoundarycomponents — useLayoutfor those casesuseRouteLoaderData - Never use — this is incorrect
useLoaderData<Route.ComponentProps["loaderData"]>()
访问当前路由的loader数据。在路由模块中,优先使用props中的(请参阅上方“访问Loader/Action数据”部分)。
loaderData在无法直接访问路由props的深层子组件中使用:
useLoaderDatatypescript
import { useLoaderData } from "react-router";
// 在子组件中,而非路由模块的默认导出
function ProductCard() {
const { products } = useLoaderData<typeof import("../route").loader>();
return <div>{products[0].name}</div>;
}重要提示:
- 假设loader执行成功
useLoaderData - 不能在或
ErrorBoundary组件中使用——这些场景请使用LayoutuseRouteLoaderData - 切勿使用——这是错误的
useLoaderData<Route.ComponentProps["loaderData"]>()
useRouteLoaderData
useRouteLoaderData
Access loader data from parent or sibling routes by route ID. Essential for and components where is not allowed.
ErrorBoundaryLayoutuseLoaderDataType-safe pattern with :
typeoftypescript
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 or
ErrorBoundarycomponents whereLayoutis not alloweduseLoaderData
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.tstypescript
import { route } from "@react-router/dev/routes";
export default [
route("/products/:id", "./product.tsx", { id: "product-detail" }),
];通过路由ID访问父路由或兄弟路由的loader数据。在和组件中是必需的,因为这些场景不允许使用。
ErrorBoundaryLayoutuseLoaderData结合的类型安全模式:
typeoftypescript
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"
你也可以在中指定自定义ID:
routes.tstypescript
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: Useinstead ofmatch.loaderData. Thematch.dataproperty is deprecated.data
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 for enhanced form handling:
Formtypescript
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的组件增强表单处理:
Formtypescript
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 or .
Route.ComponentPropsRoute.LoaderArgs在组件中访问路由参数:
typescript
import { useParams } from "react-router";
function ProductDetail() {
const { productId } = useParams();
return <div>产品:{productId}</div>;
}注意:在路由模块中,优先从或中访问params。
Route.ComponentPropsRoute.LoaderArgsuseRevalidator
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 object to attach custom metadata to routes. This metadata is accessible via in ancestor components.
handleuseMatches()导出对象为路由附加自定义元数据。该元数据可通过祖先组件中的访问。
handleuseMatches()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 with to build breadcrumbs:
handleuseMatchestypescript
// 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>;
}结合和构建面包屑:
handleuseMatchestypescript
// 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 in .
future.v8_middleware: truereact-router.config.tsSee 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.tsfuture.v8_middleware: true有关执行顺序、错误处理和基于角色的访问模式,请参阅references/middleware.md。
SSR and Pre-rendering
SSR与预渲染
Configure SSR
配置SSR
Enable server-side rendering in :
react-router.config.tstypescript
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;在中启用服务端渲染:
react-router.config.tstypescript
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. is deprecated - just return promises directly.
defer()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 (notreturn { reviews }).return reviews
See references/streaming.md for error handling, useAsyncValue patterns, and when to use streaming.
在立即渲染关键数据的同时,流式传输非关键数据。已被废弃——直接返回promises即可。
defer()快速示例:
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 = truetypescript
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: cannot render as child routes may not be ready.
HydrateFallback<Outlet />当时,在初始hydration期间显示加载UI:
clientLoader.hydrate = truetypescript
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 / but no default component:
loaderactiontypescript
// 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 routeLink with to prevent client-side routing:
reloadDocumenttypescript
<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)。当路由导出/但无默认组件时,即为资源路由:
loaderactiontypescript
// 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 });
}
// 无默认导出 = 资源路由结合链接以阻止客户端路由:
reloadDocumenttypescript
<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 types from
Route.*./+types/<route> - Data fetching prioritizes server-side loaders
- is set correctly when needed
clientLoader.hydrate = true as const - is exported when
HydrateFallbackclientLoader.hydrate = true - Actions use server-side mutations with automatic revalidation
- Forms use component from react-router, not native
<Form><form> - Type-safe utility is used for route generation
href() - Error boundaries are implemented for route errors
- Loading states use or fetcher states
useNavigation - 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
- wrapped in
<Await>with fallback UI<Suspense> - Error handling implemented for streaming promises (or
errorElement)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 in forms
defaultValue - used for filter forms (not POST)
<Form method="get"> - Use instead of
useRouteLoaderDatain ErrorBoundary/Layout componentsuseLoaderData - Parent route data accessed via with proper undefined checks
useRouteLoaderData("route-id") - exports used for route metadata (breadcrumbs, titles, etc.)
handle - Middleware used for authentication/authorization instead of loader-only patterns
- Context API () used for sharing data between middleware and loaders
context.set/get - utility used for custom status codes (404, 201, etc.)
data() - Route IDs understood for calls
useRouteLoaderData - ,
meta,linksexports used where appropriateheaders - considered for performance-critical loaders
shouldRevalidate - Server-only code uses naming convention
.server.ts - Secrets and database clients never imported in client-accessible modules
- Routes configured in with appropriate helpers (
routes.ts,route,index,layout)prefix - Resource routes (API endpoints) export no default component
- Form validation returns errors with
data({ errors }, { status: 400 }) - Optimistic UI uses for immediate feedback
fetcher.formData
完成任何React Router 7实现前,请检查以下项:
- 所有路由模块使用中的
./+types/<route>类型Route.* - 数据获取优先使用服务端loaders
- 在需要时正确设置
clientLoader.hydrate = true as const - 当时导出
clientLoader.hydrate = trueHydrateFallback - Actions使用服务端数据变更并自动重新验证
- 表单使用react-router的组件,而非原生
<Form><form> - 类型安全的工具函数用于路由生成
href() - 为路由错误实现了错误边界
- 加载状态使用或fetcher状态
useNavigation - 除非绝对必要,否则不使用客户端数据获取
- 慢/非关键数据以promises形式返回(不等待)以进行流式渲染
- 关键数据等待返回,非关键数据流式传输
- 包裹在带有fallback UI的
<Await>中<Suspense> - 为流式promises实现了错误处理(或
errorElement)useAsyncError - 搜索参数用于可共享的UI状态(筛选、分页、搜索)
- 搜索参数在loaders中验证并解析,带有合适的默认值
- 搜索参数值从loader返回并在表单中用作
defaultValue - 筛选表单使用(而非POST)
<Form method="get"> - 在ErrorBoundary/Layout组件中使用而非
useRouteLoaderDatauseLoaderData - 父路由数据通过访问,并带有正确的undefined检查
useRouteLoaderData("route-id") - 导出用于路由元数据(面包屑、标题等)
handle - 中间件用于认证/授权,而非仅loader模式
- 上下文API()用于在中间件和loaders之间共享数据
context.set/get - 工具函数用于自定义状态码(404、201等)
data() - 理解用于调用的路由ID
useRouteLoaderData - 合适的场景使用、
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 () — hooks are for deep child components
Route.ComponentProps - Never use — this is incorrect
useLoaderData<Route.ComponentProps["loaderData"]>() - Use when setting
as constclientLoader.hydrate = true - is required when
HydrateFallbackclientLoader.hydrate = true - Return promises without awaiting to stream slow/non-critical data
- Always wrap in
<Await>with fallback UI<Suspense> - cannot be used in
useLoaderDataorErrorBoundary— useLayoutuseRouteLoaderData - Use naming for modules containing secrets or database clients
.server.ts - Middleware requires flag (v7.9.0+)
future.v8_middleware: true
- 始终优先使用服务端数据加载而非客户端
- 在路由模块中,使用props()——钩子适用于深层子组件
Route.ComponentProps - 切勿使用——这是错误的
useLoaderData<Route.ComponentProps["loaderData"]>() - 设置时使用
clientLoader.hydrate = trueas const - 当时需要
clientLoader.hydrate = trueHydrateFallback - 返回promises但不等待以流式传输慢/非关键数据
- 始终将包裹在带有fallback UI的
<Await>中<Suspense> - 不能在
useLoaderData或ErrorBoundary中使用——请使用LayoutuseRouteLoaderData - 包含密钥或数据库客户端的模块使用命名
.server.ts - 中间件需要标志(v7.9.0+) ",
future.v8_middleware: true