Remix嵌套路由 #
一、嵌套路由概述 #
Remix 的核心特性之一是嵌套路由。每个路由可以有自己的布局和数据加载,父路由和子路由可以并行加载数据。
二、创建嵌套路由 #
2.1 文件结构 #
text
app/routes/
├── _index.tsx # /
├── admin.tsx # admin布局
├── admin._index.tsx # /admin
├── admin.users.tsx # /admin/users
├── admin.settings.tsx # /admin/settings
└── admin.posts._index.tsx # /admin/posts
2.2 父布局组件 #
admin.tsx 作为布局组件:
tsx
import { Outlet, Link, useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function loader({ request }: LoaderFunctionArgs) {
return json({
user: await getCurrentUser(request),
});
}
export default function AdminLayout() {
const { user } = useLoaderData<typeof loader>();
return (
<div className="flex min-h-screen">
<aside className="w-64 bg-gray-100 p-4">
<div className="mb-4">
<p>欢迎, {user.name}</p>
</div>
<nav className="space-y-2">
<Link to="/admin" className="block p-2 hover:bg-gray-200">
仪表板
</Link>
<Link to="/admin/users" className="block p-2 hover:bg-gray-200">
用户管理
</Link>
<Link to="/admin/settings" className="block p-2 hover:bg-gray-200">
系统设置
</Link>
</nav>
</aside>
<main className="flex-1 p-6">
<Outlet />
</main>
</div>
);
}
2.3 子路由页面 #
admin._index.tsx:
tsx
export default function AdminIndex() {
return (
<div>
<h1>仪表板</h1>
<p>欢迎使用管理系统</p>
</div>
);
}
admin.users.tsx:
tsx
import { useLoaderData } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function loader({}: LoaderFunctionArgs) {
const users = await getUsers();
return json({ users });
}
export default function AdminUsers() {
const { users } = useLoaderData<typeof loader>();
return (
<div>
<h1>用户管理</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
三、Outlet组件 #
3.1 Outlet的作用 #
Outlet 组件标记子路由渲染的位置:
tsx
export default function ParentLayout() {
return (
<div>
<header>
<nav>导航栏</nav>
</header>
<main>
<Outlet /> {/* 子路由内容在这里渲染 */}
</main>
<footer>
<p>页脚</p>
</footer>
</div>
);
}
3.2 Outlet Context #
通过 context 属性向子路由传递数据:
tsx
// 父组件
export default function ParentLayout() {
const [theme, setTheme] = useState("light");
return (
<div>
<Outlet context={{ theme, setTheme }} />
</div>
);
}
// 子组件
import { useOutletContext } from "@remix-run/react";
export default function ChildPage() {
const { theme, setTheme } = useOutletContext<{
theme: string;
setTheme: (theme: string) => void;
}>();
return (
<div>
<p>当前主题: {theme}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
切换主题
</button>
</div>
);
}
四、并行数据加载 #
4.1 并行加载原理 #
当访问嵌套路由时,所有层级的 loader 会并行执行:
text
访问 /admin/users
├── admin.tsx loader ─┐
├── admin.users.tsx loader ─┼─ 并行执行
└── 返回合并后的数据 ─┘
4.2 示例 #
tsx
// admin.tsx
export async function loader({ request }: LoaderFunctionArgs) {
// 这个请求与子路由的请求并行
const user = await getCurrentUser(request);
const notifications = await getNotifications(user.id);
return json({ user, notifications });
}
// admin.users.tsx
export async function loader({ request }: LoaderFunctionArgs) {
// 这个请求与父路由的请求并行
const users = await getUsers();
return json({ users });
}
4.3 避免重复请求 #
如果子路由需要父路由的数据,可以通过 useRouteLoaderData 获取:
tsx
import { useRouteLoaderData } from "@remix-run/react";
export default function AdminUsers() {
// 获取父路由的数据
const adminData = useRouteLoaderData<typeof loader>("routes/admin");
const { users } = useLoaderData<typeof loader>();
return (
<div>
<p>当前用户: {adminData.user.name}</p>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
五、布局路由 #
5.1 无路径布局 #
使用 _ 前缀创建不生成URL的布局:
text
app/routes/
├── _index.tsx # /
├── _layout.tsx # 布局(不生成URL)
├── _layout.about.tsx # /about
├── _layout.contact.tsx # /contact
└── _layout.blog._index.tsx # /blog
_layout.tsx:
tsx
import { Outlet } from "@remix-run/react";
export default function Layout() {
return (
<div>
<header>
<nav>全局导航</nav>
</header>
<main>
<Outlet />
</main>
<footer>
<p>全局页脚</p>
</footer>
</div>
);
}
5.2 多层嵌套 #
text
app/routes/
├── _index.tsx
├── _layout.tsx # 第一层布局
├── _layout.about.tsx
├── _layout.admin.tsx # 第二层布局
├── _layout.admin._index.tsx
├── _layout.admin.users.tsx
└── _layout.admin.users.$id.tsx
六、动态嵌套路由 #
6.1 动态父路由 #
text
app/routes/
├── users.$userId.tsx # /users/:userId
├── users.$userId.posts.tsx # /users/:userId/posts
└── users.$userId.posts.$id.tsx # /users/:userId/posts/:id
tsx
// users.$userId.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const user = await getUser(params.userId);
return json({ user });
}
export default function UserLayout() {
const { user } = useLoaderData<typeof loader>();
return (
<div>
<h1>{user.name}</h1>
<nav>
<Link to="posts">文章</Link>
<Link to="settings">设置</Link>
</nav>
<Outlet />
</div>
);
}
// users.$userId.posts.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const posts = await getUserPosts(params.userId);
return json({ posts });
}
export default function UserPosts() {
const { posts } = useLoaderData<typeof loader>();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
七、嵌套表单 #
7.1 在嵌套路由中处理表单 #
tsx
// admin.settings.tsx
import { Form, useActionData } from "@remix-run/react";
import type { ActionFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const settings = Object.fromEntries(formData);
const errors = validateSettings(settings);
if (errors) {
return json({ errors }, { status: 400 });
}
await saveSettings(settings);
return redirect("/admin/settings");
}
export default function AdminSettings() {
const actionData = useActionData<typeof action>();
return (
<div>
<h1>系统设置</h1>
<Form method="post">
<div>
<label>网站名称</label>
<input name="siteName" />
{actionData?.errors?.siteName && (
<p className="error">{actionData.errors.siteName}</p>
)}
</div>
<button type="submit">保存</button>
</Form>
</div>
);
}
八、错误边界嵌套 #
8.1 层级错误处理 #
每个路由都可以有自己的错误边界:
tsx
// admin.tsx
export function ErrorBoundary() {
const error = useRouteError();
return (
<div className="p-4 bg-red-100">
<h2>管理后台错误</h2>
<p>{error.message}</p>
<Link to="/admin">返回仪表板</Link>
</div>
);
}
// admin.users.tsx
export function ErrorBoundary() {
const error = useRouteError();
return (
<div className="p-4 bg-yellow-100">
<h2>用户管理错误</h2>
<p>{error.message}</p>
<Link to="/admin/users">重试</Link>
</div>
);
}
九、最佳实践 #
9.1 合理划分层级 #
text
推荐:
app/routes/
├── _index.tsx
├── _layout.tsx
├── _layout.products._index.tsx
├── _layout.products.$id.tsx
├── _layout.admin.tsx
└── _layout.admin._index.tsx
不推荐(层级过深):
app/routes/
├── _index.tsx
├── a.b.c.d.e.tsx
└── a.b.c.d.f.tsx
9.2 共享布局数据 #
tsx
// 在根布局加载共享数据
export async function loader({ request }: LoaderFunctionArgs) {
return json({
user: await getUser(request),
settings: await getSettings(),
});
}
9.3 懒加载嵌套组件 #
tsx
import { lazy, Suspense } from "react";
const HeavyComponent = lazy(() => import("~/components/Heavy"));
export default function Page() {
return (
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
);
}
十、总结 #
本章我们学习了:
- 嵌套路由结构:文件命名和目录组织
- Outlet组件:子路由渲染位置
- 并行数据加载:优化性能
- 布局路由:无路径布局组件
- 错误边界嵌套:层级错误处理
核心要点:
- 使用
Outlet渲染子路由 - 嵌套路由的 loader 并行执行
- 使用
_前缀创建无路径布局 - 每个路由可以有独立的错误边界
最后更新:2026-03-28