Files
gh-greyhaven-ai-claude-code…/skills/tanstack-patterns/examples/router-patterns.md
2025-11-29 18:29:26 +08:00

5.5 KiB

TanStack Router Patterns

Complete examples for file-based routing, layouts, and navigation.

File-Based Routing Structure

src/routes/
├── __root.tsx              # Root layout (wraps all routes)
├── index.tsx               # Homepage (/)
├── _authenticated/         # Protected routes group (underscore prefix)
│   ├── _layout.tsx        # Auth layout wrapper
│   ├── dashboard.tsx      # /dashboard
│   ├── profile.tsx        # /profile
│   └── settings/
│       ├── index.tsx      # /settings
│       └── billing.tsx    # /settings/billing
├── auth/
│   ├── login.tsx          # /auth/login
│   └── signup.tsx         # /auth/signup
└── users/
    ├── index.tsx          # /users
    └── $userId.tsx        # /users/:userId (dynamic param)

Root Layout (__root.tsx)

// src/routes/__root.tsx

import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// Create QueryClient with Grey Haven defaults
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60000, // 1 minute default stale time
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="min-h-screen bg-background">
        <Outlet /> {/* Child routes render here */}
      </div>
      <ReactQueryDevtools initialIsOpen={false} />
      <TanStackRouterDevtools position="bottom-right" />
    </QueryClientProvider>
  );
}

Route Layouts (_layout.tsx)

// src/routes/_authenticated/_layout.tsx

import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { Header } from "~/lib/components/layout/Header";
import { Sidebar } from "~/lib/components/layout/Sidebar";
import { getSession } from "~/lib/server/functions/auth";

export const Route = createFileRoute("/_authenticated/_layout")({
  // Loader runs on server for data fetching
  beforeLoad: async ({ context }) => {
    const session = await getSession();

    if (!session) {
      throw redirect({
        to: "/auth/login",
        search: {
          redirect: context.location.href,
        },
      });
    }

    return { session };
  },
  component: AuthenticatedLayout,
});

function AuthenticatedLayout() {
  const { session } = Route.useRouteContext();

  return (
    <div className="flex min-h-screen">
      <Sidebar user={session.user} />
      <div className="flex-1">
        <Header user={session.user} />
        <main className="p-6">
          <Outlet /> {/* Child routes render here */}
        </main>
      </div>
    </div>
  );
}

Page Routes with Loaders

// src/routes/_authenticated/dashboard.tsx

import { createFileRoute } from "@tanstack/react-router";
import { getDashboardData } from "~/lib/server/functions/dashboard";
import { DashboardStats } from "~/lib/components/dashboard/DashboardStats";

export const Route = createFileRoute("/_authenticated/dashboard")({
  // Loader fetches data on server before rendering
  loader: async ({ context }) => {
    const tenantId = context.session.tenantId;
    return await getDashboardData(tenantId);
  },
  component: DashboardPage,
});

function DashboardPage() {
  const data = Route.useLoaderData(); // Type-safe loader data

  return (
    <div>
      <h1 className="text-2xl font-bold">Dashboard</h1>
      <DashboardStats data={data} />
    </div>
  );
}

Dynamic Routes ($param.tsx)

// src/routes/users/$userId.tsx

import { createFileRoute } from "@tanstack/react-router";
import { getUserById } from "~/lib/server/functions/users";
import { UserProfile } from "~/lib/components/users/UserProfile";

export const Route = createFileRoute("/users/$userId")({
  // Access route params in loader
  loader: async ({ params, context }) => {
    const { userId } = params;
    const tenantId = context.session.tenantId;
    return await getUserById(userId, tenantId);
  },
  component: UserPage,
});

function UserPage() {
  const user = Route.useLoaderData();
  const { userId } = Route.useParams(); // Also available in component

  return (
    <div>
      <h1 className="text-2xl font-bold">{user.name}</h1>
      <UserProfile user={user} />
    </div>
  );
}

Navigation

import { Link, useNavigate } from "@tanstack/react-router";

function Navigation() {
  const navigate = useNavigate();

  return (
    <nav>
      {/* Type-safe Link component */}
      <Link to="/" className="...">
        Home
      </Link>

      <Link
        to="/users/$userId"
        params={{ userId: "123" }}
        className="..."
      >
        User Profile
      </Link>

      {/* Programmatic navigation */}
      <button
        onClick={() => {
          navigate({
            to: "/dashboard",
            replace: true, // Replace history entry
          });
        }}
      >
        Go to Dashboard
      </button>
    </nav>
  );
}

Key Patterns

Underscore Prefix for Groups

  • _authenticated/ - Route group (doesn't add to URL)
  • _layout.tsx - Layout wrapper for group

beforeLoad vs loader

  • beforeLoad - Auth checks, redirects
  • loader - Data fetching

Type Safety

  • Route params are type-safe
  • Loader data is type-safe
  • Navigation is type-safe