From frontend-react
Routing patterns for Next.js App Router (file-based routing, layouts, middleware, parallel/intercepting routes) and React Router v6+ (createBrowserRouter, loaders, actions, nested routes).
How this skill is triggered — by the user, by Claude, or both
Slash command
/frontend-react:react-routingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
All routes live under the `app/` directory. Every folder in `app/` is a route segment; each segment can export special files that Next.js recognises:
All routes live under the app/ directory. Every folder in app/ is a route segment; each segment can export special files that Next.js recognises:
| File | Purpose |
|---|---|
page.tsx | UI for the route; makes the segment publicly accessible |
layout.tsx | Shared UI that wraps children; persists across navigations in its subtree |
loading.tsx | Suspense fallback for the segment |
error.tsx | Error boundary for the segment (must be a Client Component) |
not-found.tsx | Renders when notFound() is called or no route matches |
route.ts | API route handler (GET, POST, etc.) |
template.tsx | Like layout but remounts on navigation |
default.tsx | Fallback for parallel routes |
// app/layout.tsx — root layout (required)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/dashboard/layout.tsx — nested layout
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
<html> and <body> tags.children.app/
posts/
[id]/
page.tsx → /posts/123
[...slug]/
page.tsx → /posts/a/b/c
[[...slug]]/
page.tsx → /posts OR /posts/a/b (optional catch-all)
// app/posts/[id]/page.tsx
interface Props {
params: Promise<{ id: string }>;
}
export default async function PostPage({ params }: Props) {
const { id } = await params;
const post = await fetchPost(id);
return <article>{post.title}</article>;
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(p => ({ id: p.id }));
}
Use (groupName) folders to organise routes without affecting the URL path. Useful for sharing a layout among a subset of routes.
app/
(marketing)/
layout.tsx → marketing layout (no URL segment)
page.tsx → /
about/page.tsx → /about
(app)/
layout.tsx → authenticated app layout
dashboard/page.tsx → /dashboard
Render multiple pages simultaneously in the same layout using named @slot folders:
app/
layout.tsx
@modal/
(.)photo/[id]/page.tsx → intercepting route
@analytics/
page.tsx
page.tsx
// app/layout.tsx
export default function Layout({
children,
modal,
analytics,
}: {
children: React.ReactNode;
modal: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{modal}
<aside>{analytics}</aside>
</>
);
}
Intercept a route to show it in a modal while keeping the current page in the background. Uses (.), (..), (..)(..), (...) conventions:
| Syntax | Intercepts |
|---|---|
(.)segment | same level |
(..)segment | one level above |
(..)(..)segment | two levels above |
(...)segment | from root |
app/
feed/
page.tsx
@modal/
(.)photo/[id]/
page.tsx ← renders as modal; direct nav shows full page
photo/
[id]/
page.tsx ← full page when navigated to directly
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return <DashboardSkeleton />;
}
// app/dashboard/error.tsx — must be a Client Component
"use client";
import { useEffect } from "react";
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<p>Failed to load dashboard.</p>
<button onClick={reset}>Retry</button>
</div>
);
}
// app/not-found.tsx
export default function NotFound() {
return <h2>404 – Page Not Found</h2>;
}
middleware.ts at the project root (or src/) runs before route rendering:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};
matcher to limit which paths trigger middleware.Prefer createBrowserRouter (data router) over <BrowserRouter> for access to loaders and actions.
// router.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{
path: "dashboard",
element: <DashboardLayout />,
loader: dashboardLoader,
children: [
{ index: true, element: <DashboardHome /> },
{
path: "settings",
element: <SettingsPage />,
loader: settingsLoader,
action: settingsAction,
},
],
},
],
},
]);
export function App() {
return <RouterProvider router={router} />;
}
Loaders run before the component renders; data is available via useLoaderData.
import { useLoaderData, type LoaderFunctionArgs } from "react-router-dom";
export async function dashboardLoader({ params }: LoaderFunctionArgs) {
const data = await fetchDashboardData(params.id);
if (!data) throw new Response("Not Found", { status: 404 });
return data;
}
export function DashboardPage() {
const data = useLoaderData() as DashboardData;
return <div>{data.title}</div>;
}
Actions handle form submissions and mutations; triggered by <Form> or useFetcher.
import { Form, redirect, type ActionFunctionArgs } from "react-router-dom";
export async function settingsAction({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get("name") as string;
await updateUser({ name });
return redirect("/dashboard");
}
export function SettingsPage() {
return (
<Form method="post">
<input name="name" />
<button type="submit">Save</button>
</Form>
);
}
<Outlet /> renders the matched child route within a layout:
import { Outlet } from "react-router-dom";
export function DashboardLayout() {
return (
<div>
<DashboardNav />
<main>
<Outlet /> {/* child route renders here */}
</main>
</div>
);
}
import Link from "next/link";
import { useRouter } from "next/navigation";
import { redirect } from "next/navigation"; // server-side
// Declarative link
<Link href="/dashboard">Dashboard</Link>
<Link href={{ pathname: "/posts/[id]", query: { id: "42" } }}>Post</Link>
// Programmatic (Client Component)
"use client";
const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.back();
router.refresh(); // revalidate Server Component data
// Server-side redirect (Server Component or Server Action)
redirect("/login");
import { Link, NavLink, useNavigate } from "react-router-dom";
// Declarative
<Link to="/dashboard">Dashboard</Link>
<NavLink to="/dashboard" className={({ isActive }) => isActive ? "active" : ""}>
Dashboard
</NavLink>
// Programmatic
const navigate = useNavigate();
navigate("/dashboard");
navigate(-1); // go back
navigate("/login", { replace: true });
| Do | Don't |
|---|---|
| Use file-based routing in Next.js App Router | Mix App Router and Pages Router in same project |
Keep error.tsx as a Client Component | Forget "use client" on error boundaries |
Use generateStaticParams for known dynamic routes | Skip static generation for highly-repetitive static pages |
Use middleware.ts matcher to limit scope | Run heavy logic unconditionally in middleware |
Prefer createBrowserRouter over <BrowserRouter> in new React Router projects | Use useEffect to fetch data when a loader can do it |
Use <Outlet /> for layout nesting | Re-implement layout wrapping manually per route |
npx claudepluginhub gagandeepp/software-agent-teams --plugin frontend-reactGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.