AgentSkillsCN

frontend-route-layout

当您需要创建或修改路由层级的布局、路由布局、使用 TanStack Router 的 Outlet 模式封装共享页面,或当一个组件同时包裹多个页面时,可使用此技能。

SKILL.md
--- frontmatter
name: frontend-route-layout
description: Use when creating or modifying route-level layouts, layout routes, shared page wrappers using TanStack Router's Outlet pattern, or when a component wraps multiple pages

Frontend: Route Layouts (TanStack Router)

Route layouts are layout routes that wrap a group of child routes with shared UI (navigation, sidebars, theme wrappers). They use TanStack Router's file-based _prefix convention and render children via <Outlet />.

CRITICAL: Never use wrapper components for layouts. If multiple routes share a visual frame (nav, sidebar, container), it MUST be a layout route — not a component that each page imports and wraps around its content.

Layout Route Pattern

File Convention

Layout routes use underscore _ prefix — this creates a route node with NO URL path segment:

code
src/routes/
├── _auth.tsx              # Layout: auth pages (no URL segment)
├── _auth/
│   ├── login.tsx          # URL: /login  (not /_auth/login)
│   └── signup.tsx         # URL: /signup
├── _protected.tsx         # Layout: authenticated pages
├── _protected/
│   ├── index.tsx          # URL: /
│   └── $organizationId/  # URL: /$organizationId/...

The _ prefix is pathless — child routes get clean URLs. _auth/login.tsx resolves to /login, not /_auth/login.

Layout Route File

tsx
// src/routes/_auth.tsx
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { theme as antdTheme } from "antd";

export const Route = createFileRoute("/_auth")({
    component: AuthLayout,
});

function AuthLayout() {
    const { token } = antdTheme.useToken();

    return (
        <div style={{ /* shared layout styles */ }}>
            {/* Shared UI: sidebars, navs, decorations */}
            <Outlet />  {/* Child route renders here */}
        </div>
    );
}

Child Route File

tsx
// src/routes/_auth/login.tsx
import { createFileRoute } from "@tanstack/react-router";
import { Page_Login } from "@/pages/Page_Login/Page_Login";

export const Route = createFileRoute("/_auth/login")({
    beforeLoad: async () => { /* guards */ },
    component: Page_Login,
});

Page components become pure content — no layout wrapping, no shared chrome.

Existing Layout Routes

LayoutFilePurposeChildren
_protected_protected.tsxAuth guard + nav + sized containerAll authenticated pages
_auth_auth.tsxAuth page frame (carousel + form split)/login, /signup

When to Create a Layout Route

Create a layout route when:

  • 2+ routes share the same visual wrapper (nav, sidebar, split layout)
  • Routes need a common beforeLoad guard
  • A group of pages needs a shared container/theme treatment

Do NOT create a layout route when:

  • Only one page uses the layout → inline it in the page component
  • The "layout" is just a provider → put it in __root.tsx or a parent layout

Common Mistakes

Wrapper Components Instead of Layout Routes

tsx
// ❌ WRONG — wrapper component imported per page
export const Page_Login = () => (
    <AuthLayout>
        <LoginForm />
    </AuthLayout>
);

export const Page_SignUp = () => (
    <AuthLayout>
        <SignUpForm />
    </AuthLayout>
);
tsx
// ✅ CORRECT — layout route with Outlet
// _auth.tsx provides the layout
// Page_Login and Page_SignUp are pure content, no wrapper
export const Page_Login = () => <LoginForm />;

Missing Outlet

tsx
// ❌ WRONG — layout without Outlet (children never render)
function ProtectedLayout() {
    return <Nav />;
}

// ✅ CORRECT — Outlet renders child route
function ProtectedLayout() {
    return (
        <>
            <Nav />
            <Outlet />
        </>
    );
}

Wrong createFileRoute Path

tsx
// ❌ WRONG — path doesn't match file location
// File: src/routes/_auth/login.tsx
createFileRoute("/login")

// ✅ CORRECT — matches file-based route tree
createFileRoute("/_auth/login")

Layout Height Contract

Layout routes define the container size; child pages use relative heights:

tsx
// Layout route provides sized container
function ProtectedLayout() {
    return (
        <div style={{ height: `calc(100vh - ${NAV_HEIGHT}px)` }}>
            <Outlet />
        </div>
    );
}

// Page uses height: 100% (NEVER recalculate viewport)
function Page_Example() {
    return <div style={{ height: "100%" }}>{/* content */}</div>;
}

See frontend-page-layout for full height calculation rules.

Route Tree Regeneration

After creating/moving route files, regenerate the route tree:

bash
pnpm --filter my-vite-app dev  # Auto-regenerates on file change
# Or manually trigger by saving any route file during dev server

The generated routeTree.gen.ts is auto-managed — never edit it manually.

Related Skills

  • frontend-routing — Route naming, params, guards, navigation
  • frontend-page-layout — Page-level height calculations and flex patterns
<!-- Last created: 2026-02-08 -->