--- description: Next.js routing with typed routes, PageProps, LayoutProps helpers, and nuqs for URL state. Use for pages, layouts, navigation, and query parameters. name: nextjs metadata: skiller: source: .agents/rules/nextjs.mdc --- # Next.js Routing & Navigation ## Requirements ### Route Props Helpers (Next.js 15.5+) - Use globally available `PageProps`, `LayoutProps`, `RouteContext` types - no imports needed - **RouteContext**: Use for typing route handler context parameters with proper type inference - **PageProps**: Use for page components with typed params and searchParams - **LayoutProps**: Use for layout components with children and parallel route slots - Automatically includes typed params, searchParams, children, and parallel route slots - Always await Promise-based params and searchParams ### Typed Routes (Next.js 15.5+) - TypeScript will catch invalid routes in `` components at compile time - Automatically generates types based on your file structure - Use `Route` type for props - Use `as Route` for non-literal strings (e.g., `('/blog' + slug) as Route`) ### Custom Param Hooks - **NEVER use useParams from Next.js directly** - use typed alternatives from `@/hooks/use-params` - **Use unique param names** (e.g., `[userId]` not `[id]`) for better type inference - **useTParams()**: For specific app routes with exact typing - **useLayoutParams()**: For layout components, returns all params as optional - **useLayoutParams()**: For layout components with route prefix, exact route params required + prefix params optional ### URL Query State - **NEVER use useSearchParams from Next.js** - always use nuqs for URL query state - Use appropriate parsers based on parameter data type - Configure history mode and clearOnDefault - Handle URL updates with void to prevent promise-related linting issues ## Examples // Page component with PageProps export default async function Page(props: PageProps<'/products/[category]/[id]'>) { const { category, id } = await props.params; const searchParams = await props.searchParams; return (

Product: {id}

Category: {category}

); }
// Layout with parallel routes export default function DashboardLayout(props: LayoutProps<'/dashboard'>) { return (
{/* Typed parallel route */}
{props.children}
{props.analytics}
{/* Typed parallel route */}
); }
// Route handler with RouteContext (recommended) export async function GET( request: Request, ctx: RouteContext<'/api/posts/[slug]'> ) { const { slug } = await ctx.params; return Response.json({ slug }); } // Multiple parameters export async function DELETE( request: Request, ctx: RouteContext<'/api/users/[id]/posts/[postId]'> ) { const { id, postId } = await ctx.params; return Response.json({ userId: id, postId }); } // Optional parameters export async function PUT( request: Request, ctx: RouteContext<'/api/categories/[[...slug]]'> ) { const { slug } = await ctx.params; // slug: string[] | undefined return Response.json({ segments: slug }); } // URL Query State with nuqs export const useFilterState = () => { return useQueryState( 'filter', parseAsStringEnum(['all', 'active', 'completed']) .withDefault('all') .withOptions({ history: 'push', clearOnDefault: true }) ); }; // Usage const [filter, setFilter] = useFilterState(); void setFilter('active'); // Enable Typed Routes in next.config.ts const nextConfig = { typedRoutes: true, // Compile-time type safety for routes }; export default nextConfig; // Usage in components import Link from 'next/link'; // ✅ Type-safe links View Patient Browse Library // ✅ Non-literal strings with Route type const slug = 'nextjs'; Blog Post router.push(('/blog/' + slug) as Route); // ❌ TypeScript will catch invalid routes at compile time Broken Link // ← Type error // Custom Param Hooks Usage import { useTParams, useLayoutParams } from '@/hooks/use-params'; // ✅ For specific routes with exact typing const PatientPage = () => { const params = useTParams<'/patients/[patientId]'>(); params.patientId; // string - guaranteed to exist }; // ✅ For layouts - all params optional const RootLayout = ({ children }) => { const params = useLayoutParams(); params.patientId; // string | undefined params.complaintId; // string | undefined }; // ✅ For layouts with route prefix - exact + optional const ComplaintLayout = ({ children }) => { const params = useLayoutParams<'/complaints/[complaintId]'>(); params.complaintId; // string - required for exact match params.someOtherParam; // string | undefined - from related routes }; // ❌ Don't use manual typing for route handlers export async function GET( request: Request, { params }: { params: Promise<{ slug: string }> } ) { const { slug } = await params; return Response.json({ slug }); } // ❌ Don't use manual typing for page props interface Props { params: Promise<{ slug: string }>; children: React.ReactNode; } // ❌ Don't use raw useParams - use typed alternatives import { useParams } from 'next/navigation'; const params = useParams(); // ❌ Don't use useSearchParams import { useSearchParams } from 'next/navigation'; const searchParams = useSearchParams(); // ❌ Don't use string concatenation for routes router.push(`/patients/${id}`); // Use typed routes instead