5.1 嵌套路由的本质
Remix 的嵌套路由远不只是"嵌套布局",它实现了 UI 嵌套与数据嵌套的统一。每层路由不仅提供布局组件,还有独立的 loader,这些 loader 在同一次页面请求中并行执行,而不是等父路由数据加载完再加载子路由数据。
并行 vs 串行访问 /admin/users/123 时,Remix 会同时发起 admin.tsx loader、admin.users.tsx loader、admin.users.$id.tsx loader 三个请求,总耗时是最慢那个,而非三者之和。这是 Remix 嵌套路由最大的性能优势。
路由树示意
访问 /admin/users/123 的渲染树:
root.tsx # HTML 骨架 + 全局导航
└── _admin.tsx # 后台布局(侧边栏 + 顶部栏)
└── admin.users.tsx # 用户列表布局(含列表侧边)
└── admin.users.$id.tsx # 用户详情内容区
并行执行的 loader:
root loader → 当前用户信息
admin loader → 后台菜单权限
users loader → 用户列表
users.$id loader → 单个用户详情
TEXT
5.2 Outlet context — 父子路由传值
父路由可以通过 <Outlet context={value} /> 向子路由传递数据,子路由通过 useOutletContext() 获取。这避免了通过 URL 参数或全局状态传递内部数据。
// app/routes/admin.users.tsx — 父路由传递 context
import { Outlet, useLoaderData } from "@remix-run/react";
type UserListContext = {
refetchTrigger: number;
onUserUpdated: () => void;
};
export default function UsersLayout() {
const { users } = useLoaderData<typeof loader>();
return (
<div className="users-layout">
<aside>
{users.map(u => <UserListItem key={u.id} user={u} />)}
</aside>
<main>
{/* 将父路由数据传给子路由 */}
<Outlet context={{ users } satisfies UserListContext} />
</main>
</div>
);
}
TSX
// app/routes/admin.users.$id.tsx — 子路由接收 context
import { useOutletContext } from "@remix-run/react";
export default function UserDetail() {
const { users } = useOutletContext<UserListContext>();
// 可以访问父路由传来的 users 数据
return <div>...</div>;
}
TSX
5.3 路由级错误边界 ErrorBoundary
每条路由都可以导出 ErrorBoundary 组件。当该路由的 loader、action 或组件渲染抛出错误时,Remix 会渲染该路由的 ErrorBoundary,而不影响父路由的正常显示。这实现了精确的错误隔离。
// app/routes/blog.$slug.tsx
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
// 处理从 loader/action 抛出的 Response 对象
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} — {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
// 处理意外的 JS 错误
if (error instanceof Error) {
return (
<div>
<h1>出错了</h1>
<p>{error.message}</p>
</div>
);
}
return <h1>未知错误</h1>;
}
TSX
错误隔离效果如果 /blog/not-found 的 loader 抛出 404,只有 blog.$slug.tsx 的 ErrorBoundary 被渲染,导航栏、页脚、博客列表侧边栏等父路由 UI 仍然正常显示,用户不会看到整个页面崩溃。
5.4 实战:后台管理三级嵌套布局
构建一个典型的后台管理系统,演示三层嵌套路由的完整实现。
app/routes/
├── _admin.tsx # 无 URL 的后台主布局(侧边栏+顶栏)
├── _admin._index.tsx # /(后台首页仪表盘)
├── _admin.users.tsx # /users(用户管理布局)
├── _admin.users._index.tsx # /users(用户列表)
├── _admin.users.$id.tsx # /users/:id(用户详情)
├── _admin.users.new.tsx # /users/new(新建用户)
└── _admin.settings.tsx # /settings(系统设置)
STRUCTURE
// app/routes/_admin.tsx — 后台主布局
import { Outlet, NavLink } from "@remix-run/react";
import { requireUser } from "~/session.server";
export async function loader({ request }: LoaderFunctionArgs) {
// 路由守卫:未登录跳转到 /login
const user = await requireUser(request);
return json({ user });
}
export default function AdminLayout() {
const { user } = useLoaderData<typeof loader>();
return (
<div className="admin-shell">
<aside className="sidebar">
<p>{user.name}</p>
<NavLink to="/">仪表盘</NavLink>
<NavLink to="/users">用户管理</NavLink>
<NavLink to="/settings">系统设置</NavLink>
</aside>
<main className="admin-content">
<Outlet />
</main>
</div>
);
}
TSX
// app/routes/_admin.users.tsx — 用户管理布局
import { Outlet, useLoaderData, NavLink } from "@remix-run/react";
export async function loader() {
const users = await getUsers();
return json({ users });
}
export default function UsersLayout() {
const { users } = useLoaderData<typeof loader>();
return (
<div className="users-layout">
<nav className="user-list">
<NavLink to="new">+ 新建用户</NavLink>
{users.map(u => (
<NavLink key={u.id} to={`${u.id}`}>{u.name}</NavLink>
))}
</nav>
<section>
{/* 用户详情/新建表单在这里渲染 */}
<Outlet />
</section>
</div>
);
}
TSX
5.5 NavLink 激活状态
Remix 的 <NavLink> 会根据当前 URL 自动添加 active class(或通过 className 函数自定义),无需手动管理激活状态。
import { NavLink } from "@remix-run/react";
// 方式一:自动添加 .active class
<NavLink to="/users">用户管理</NavLink>
// 方式二:函数式 className
<NavLink
to="/users"
className={({ isActive, isPending }) =>
isActive ? "nav-link active" : isPending ? "nav-link pending" : "nav-link"
}
>
用户管理
</NavLink>
TSX
本章小结Remix 嵌套路由的精髓:UI 树与数据加载树完全对应;各层 loader 并行执行,无串行等待;Outlet context 让父路由向子路由传值;每条路由独立的 ErrorBoundary 实现精准错误隔离,父路由 UI 不受影响。这套机制是 Remix 在性能和架构上优于其他框架的核心所在。