Skip to content

wcz-layout

routing

96 linesSource

Use when adding or changing a page, route file, navigation entry, search params, or route-level access control. Covers createFileRoute, beforeLoad requireAuth, loader preloads, validateSearch, useSearch, src/routes/ naming and colocation, __root.tsx navigation and LayoutProvider options (app bar actions, showShell), the auth and login routes, and the RouterLink/RouterButton link components.

Mechanics (route definitions, loaders, context, guards, navigation, search params, route-tree fundamentals) are covered by TanStack's own skills. List the skills shipped by @tanstack/router-core and @tanstack/react-router and load whichever cover the task at hand. This skill adds the wcz-layout conventions.

Rules

  • Include requireAuth(<permissionKey>) in the route's beforeLoad to enforce access control — pass a key defined in your app's src/lib/auth/permissions.ts. Use bare requireAuth() when any signed-in user may enter, and omit it entirely only for intentionally public routes.
  • Route-level requireAuth only hides the UI. Every server function or server route that accesses protected data still needs authMiddleware; see the server-functions skill.
  • Use Route.useRouteContext() to access the authenticated user. user can be null if the route doesn't include requireAuth.
  • Preload the route's live queries in the loader and return the promise: loader: ({ context }) => context.dbClient.preloadLiveQuery(<x>QueryOptions). Detail routes await it and throw notFound() for a missing row; see the client-db skill.
  • Literal URL segments and feature folders use kebab-case. Entity feature names are plural; TanStack special route tokens retain their required syntax.
  • If a feature contains only one route, create a single file route: src/routes/<feature>s.tsx
  • If a feature contains multiple related routes, create a folder: src/routes/<feature>s/index.tsx
  • Colocate route-specific components/hooks inside routes/<feature>s/-components or -hooks.
  • When a route needs its own components/hooks that no sibling route uses, wrap it in a route group folder named after the route and colocate them there: routes/<feature>s/(index)/index.tsx with (index)/-components and (index)/-hooks.
  • Root-level components/ and hooks/ are only for code shared across multiple routes.
  • After creating a new route, add a navigation item with a unique icon from @mui/icons-material in src/routes/__root.tsx.
  • Navigation kinds: item (title, icon, to + params/search, or href for an external URL), group (title, icon, children), header (title) and divider.
  • Hide a navigation entry from users who cannot open it: hidden: !hasPermission(user, <key>) with the same key as its route's requireAuth(<key>).
  • LayoutProvider in __root.tsx takes theme, navigation and options: appBarActions (nodes between the app title and the account menu) and showShell (app bar + navigation visibility).
  • For user-clickable in-app navigation, use the type-safe router components from wcz-layout/components (RouterButton, RouterIconButton, RouterLink, RouterFab, RouterTab, RouterListItemButton) rather than raw MUI with a manual href or an onClick with navigate(). They wrap createLink, so to/params/search are type-checked and the router's defaultPreload: "intent" preloads the target's loader on hover/focus. They accept the matching MUI props. Keep useNavigate() for navigation after an action (e.g. after a successful save).

Examples

tsx
// imports
import AccountTree from "@mui/icons-material/AccountTree";
import Code from "@mui/icons-material/Code";
import Home from "@mui/icons-material/Home";
import Widgets from "@mui/icons-material/Widgets";
import { createFileRoute } from "@tanstack/react-router";
import type { Navigation } from "wcz-layout";
import { useDialogs, useTranslation } from "wcz-layout/hooks";
import { hasPermission, requireAuth } from "wcz-layout/utils";

// src/routes/<feature>s/index.tsx
export const Route = createFileRoute("/<feature>s/")({
  component: RouteComponent,
  beforeLoad: requireAuth("admin"),
  loader: ({ context }) => context.dbClient.preloadLiveQuery(featuresQueryOptions),
});

function RouteComponent() {
  const { t } = useTranslation();
  const { alert, confirm } = useDialogs();
  const { user } = Route.useRouteContext();
  // route component code...
}

// __root.tsx navigation
const navigation: Navigation = [
  { kind: "item", to: "/", title: t("Home"), icon: <Home /> },
  { kind: "header", title: t("Documentation") },
  {
    kind: "group",
    title: t("Components"),
    icon: <Widgets />,
    hidden: !hasPermission(user, "admin"),
    children: [
      { kind: "item", to: "/components/navigation", title: t("Navigation"), icon: <AccountTree /> },
      { kind: "divider" },
      { kind: "item", to: "/components/forms", title: t("Forms"), icon: <Code /> },
    ],
  },
];