---
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