AgentSkillsCN

frontend-routing

当您需要创建或修改路由、路由守卫,或进行导航操作时,可使用此技能。

SKILL.md
--- frontmatter
name: frontend-routing
description: Use when creating or modifying routes, route guards, or navigation

Frontend: TanStack Router

All routing uses TanStack Router with file-based routing. Follow these patterns for route naming, guards, params, and navigation.

File-Based Routing Structure

code
src/routes/
├── __root.tsx                          # Root layout (double underscore)
├── _protected.tsx                      # Layout route (single underscore)
├── _protected/
│   ├── index.tsx                       # Index route
│   └── $organizationId.tsx             # Dynamic param route
├── _auth.tsx                           # Layout route
├── _auth/
│   ├── login.tsx                       # Public route under layout
│   └── signup.tsx                      # Public route under layout
└── not-whitelisted.tsx                 # Standalone public route

Naming rules:

  • Root route: __root.tsx (double underscore)
  • Layout routes: Prefix with _ (e.g., _protected.tsx) — see frontend-route-layout
  • Dynamic params: Use $paramName syntax (e.g., $organizationId.tsx)
  • Index routes: Use index.tsx within folder

Route Guards with beforeLoad

Pattern: Use beforeLoad for authentication and authorization checks.

tsx
export const Route = createFileRoute("/_protected")({
    beforeLoad: async ({ location }) => {
        const { data: { session } } = await supabase.auth.getSession();

        if (!session) {
            const redirectPath = `${location.pathname}${location.search ? `?${new URLSearchParams(location.search).toString()}` : ""}`;
            throw redirect({ to: "/login", search: { redirect: redirectPath } });
        }

        // Authorization — direct DB query, no cache
        const { data: profile } = await supabase
            .from("profiles")
            .select("whitelisted")
            .eq("id", session.user.id)
            .single();

        if (!profile?.whitelisted) {
            throw redirect({ to: "/not-whitelisted" });
        }
    },
    component: RouteComponent,
});

Key rules:

  • Always use beforeLoad for security checks (never component-level)
  • Preserve redirect path for post-login UX
  • Direct database queries for security-critical checks (no cache dependency)

Dynamic Param Routes with Access Control

tsx
export const Route = createFileRoute("/_protected/$organizationId")({
    beforeLoad: async ({ params }) => {
        const { organizationId } = params;
        const { data: { session } } = await supabase.auth.getSession();
        if (!session) throw redirect({ to: "/login" });

        const { data: membership } = await supabase
            .from("organization_members")
            .select("*")
            .eq("user_id", session.user.id)
            .eq("organization_id", organizationId)
            .maybeSingle();

        if (!membership) throw redirect({ to: "/access-denied" });
    },
    component: Page_Organization,
});
  • Use maybeSingle() to handle no-match gracefully
  • Immediate redirect on unauthorized access

Public Routes with Redirect Logic

tsx
const LoginSearchSchema = z.object({
    redirect: z.string().optional().catch(undefined),
});

export const Route = createFileRoute("/_auth/login")({
    validateSearch: LoginSearchSchema.parse,
    beforeLoad: async ({ search }) => {
        const { data: { session } } = await supabase.auth.getSession();
        if (session) throw redirect({ to: search.redirect || "/" });
    },
    component: Page_Login,
});
  • Validate search schema with Zod
  • Redirect authenticated users away from login

Extracting Route Params (useParams)

ALWAYS use the from option for full TypeScript type safety:

typescript
// ✅ Correct - Full type safety
const { organizationId, projectId } = useParams({
    from: "/_protected/$organizationId/$projectId",
});

// ❌ Wrong - Loses all type safety
const { organizationId } = useParams({ strict: false }) as { organizationId: string };

Anti-Pattern Detection

  1. _root.tsx (single underscore) → ✅ __root.tsx (double)
  2. :organizationId or {organizationId} → ✅ $organizationId
  3. ❌ Component-level auth checks → ✅ Use beforeLoad
  4. ❌ Cache-based security → ✅ Direct database query
  5. ❌ Redirect to /login without preserving path → ✅ Pass location.pathname as search param
  6. useParams({ strict: false }) → ✅ useParams({ from: '/_protected/$organizationId' })
  7. ❌ Wrapper components for shared layouts → ✅ Layout routes (see frontend-route-layout)

Related Skills

  • frontend-route-layout — Layout route patterns, Outlet, _prefix convention
  • frontend-page-layout — Page-level height calculations
  • frontend-supabase-auth — Auth session management
<!-- Last compacted: 2026-02-08 -->