| name | next-js |
| description | [Applies to: **/*.{js,jsx,ts,tsx}] This guide defines definitive best practices for Next.js applications, focusing on the `app` directory, Server Components, performance, and maintainability. Follow these rules to build robust, scalable, and performant Next.js projects. |
| source | cursor_mdc |
next-js Best Practices
This document outlines the definitive best practices for developing Next.js applications. Adhering to these guidelines ensures consistent, performant, and maintainable code, leveraging Next.js's strengths for modern web development.
1. Code Organization and Structure
Always use the app/ directory for new projects. Organize code by feature, not by type, to improve discoverability and cohesion.
✅ GOOD: Feature-Driven app/ Directory
Group all related files for a feature (components, pages, layouts, hooks, types) within a single directory.
export default function DashboardPage() { }
export default function DashboardLayout({ children }) { }
export function DashboardOverview() { }
export function useDashboardData() { }
❌ BAD: Type-Driven app/ Directory
Avoid scattering files of the same feature across different top-level type directories.
Core Directories
app/: All route-related files (page.tsx, layout.tsx, loading.tsx, error.tsx, route.ts).
components/: Reusable UI components that are not directly tied to a specific route.
lib/: Backend-agnostic utility functions, data access layers, and third-party integrations.
hooks/: Custom React hooks for reusable logic.
types/: Global TypeScript type definitions and interfaces.
public/: Static assets (images, fonts) that are served directly.
2. Component Architecture: Server Components First
Prioritize Server Components for all UI rendering. Use Client Components only when interactivity (state, effects, event handlers) is strictly required.
✅ GOOD: Server Component by Default
Server Components reduce client-side JavaScript bundles, improve initial page load, and enhance security by keeping sensitive logic on the server.
import { getProductDetails } from '@/lib/api';
export default async function ProductPage({ params }) {
const product = await getProductDetails(params.id);
return (
<div>
<h1>{product.name}</h1>
{/* ... more server-rendered UI */}
<AddToCartButton productId={product.id} /> {/* Client Component */}
</div>
);
}
✅ GOOD: "use client" Boundary as Low as Possible
Place the "use client" directive at the lowest possible point in your component tree. This ensures that only the interactive parts are client-rendered, keeping parent components as Server Components.
'use client';
import { useState } from 'react';
export function AddToCartButton({ productId }) {
const [quantity, setQuantity] = useState(1);
return <button onClick={() => alert(`Added ${quantity} of ${productId}`)}>Add to Cart</button>;
}
❌ BAD: Overuse of "use client"
Don't mark entire feature folders or layouts as client components if only a small part needs interactivity. This unnecessarily increases client bundle size.
'use client';
import { useState, useEffect } from 'react';
export default function ProductPage({ params }) {
return <button>Add to Cart</button>;
}
3. Data Fetching
Fetch data directly in Server Components using fetch or a dedicated data access layer. Use Route Handlers for client-side mutations or when exposing a specific API endpoint.
✅ GOOD: Server Component Data Fetching
Directly fetch data in Server Components. fetch requests are automatically memoized and cached by Next.js.
import { getUserProfile, getRecentOrders } from '@/lib/api';
export default async function DashboardPage() {
const [user, orders] = await Promise.all([
getUserProfile(),
getRecentOrders(),
]);
return (
<div>
<h2>Welcome, {user.name}</h2>
<OrderList orders={orders} />
</div>
);
}
✅ GOOD: Route Handlers for Client-Side Mutations
Use route.ts for API endpoints that handle client-side data mutations (e.g., form submissions, API calls from client components).
import { NextResponse } from 'next/server';
import { addToCart } from '@/lib/cart';
export async function POST(request: Request) {
const { productId, quantity } = await request.json();
await addToCart(productId, quantity);
return NextResponse.json({ success: true });
}
✅ GOOD: Streaming with loading.tsx and Suspense
Improve perceived performance by showing instant loading states for slow data fetches.
export default function Loading() {
return <div>Loading dashboard...</div>;
}
import { Suspense } from 'react';
import { SlowComponent } from './components/SlowComponent';
export default async function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading slow data...</div>}>
<SlowComponent />
</Suspense>
</main>
);
}
4. Performance Considerations
Leverage Next.js's built-in optimizations for images, fonts, and code splitting.
✅ GOOD: next/image for Images
Always use next/image for local and remote images. It provides automatic optimization, lazy loading, and responsive sizing.
import Image from 'next/image';
import profilePic from '@/public/profile.jpg';
export function UserAvatar() {
return (
<Image
src={profilePic}
alt="User Profile"
width={100}
height={100}
placeholder="blur"
/>
);
}
❌ BAD: Native <img> Tag
Avoid the native <img> tag as it bypasses Next.js's image optimizations.
export function UserAvatar() {
return <img src="/profile.jpg" alt="User Profile" width="100" height="100" />;
}
✅ GOOD: next/font for Fonts
Use next/font to optimize font loading, eliminate external network requests, and prevent layout shift.
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
✅ GOOD: Dynamic Imports for Heavy Components
Lazily load heavy client-side components or third-party libraries using next/dynamic.
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./components/HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false,
});
export function DashboardCharts() {
return (
<div>
<HeavyChart />
</div>
);
}
5. State Management
Keep state local where possible. For global state, use React Context for simple cases or lightweight libraries like Zustand/Jotai for more complex needs.
✅ GOOD: Local State with useState
For component-specific, ephemeral state.
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
✅ GOOD: Global State with Context or Zustand/Jotai
For application-wide state that needs to be shared across many components. Prefer Zustand or Jotai over Redux for most Next.js projects due to their simplicity and performance.
import { create } from 'zustand';
interface BearState {
bears: number;
increasePopulation: () => void;
}
export const useBearStore = create<BearState>((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}));
6. Error Handling
Implement robust error handling using Next.js's dedicated error files.
✅ GOOD: Route-Level error.tsx
Catch errors within a specific route segment, providing localized fallback UI.
'use client';
import { useEffect } from 'react';
export default function Error({ error, reset }) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong in the dashboard!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
✅ GOOD: Global global-error.tsx
Catch uncaught errors across your entire application, providing a consistent fallback.
'use client';
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Something went wrong globally!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
✅ GOOD: not-found.tsx for 404s
Create a custom 404 page for unmatched routes.
export default function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
</div>
);
}
7. ESLint and Type Checking
Enforce code quality and catch common issues early with ESLint and TypeScript.
✅ GOOD: Use eslint-config-next/core-web-vitals
This configuration elevates performance-related warnings to errors, ensuring your application meets Core Web Vitals standards. Always combine with eslint-config-next/typescript for TypeScript projects.
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTs from 'eslint-config-next/typescript';
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
{
rules: {
'@next/next/no-img-element': 'error',
},
},
globalIgnores([
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
]),
]);
export default eslintConfig;
8. Common Pitfalls and Anti-patterns
Avoid these common mistakes to maintain a high-quality Next.js application.
❌ BAD: Using <a> for Internal Navigation
This bypasses Next.js's automatic prefetching and client-side navigation.
<a href="/dashboard">Go to Dashboard</a>
✅ GOOD: Use <Link> for Internal Navigation
Enables client-side navigation and prefetching for a smoother user experience.
import Link from 'next/link';
<Link href="/dashboard">Go to Dashboard</Link>
❌ BAD: Async Client Components
Client Components cannot be async. If you need to fetch data on the client, use useEffect or a client-side data fetching library.
'use client';
export default async function MyClientComponent() {
return <div>Client UI</div>;
}
✅ GOOD: Client-Side Data Fetching in Client Components
Use useEffect or a dedicated client-side library (like SWR or React Query) for data fetching in Client Components.
'use client';
import { useState, useEffect } from 'react';
export default function MyClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
}
fetchData();
}, []);
return <div>{data ? `Data: ${data.message}` : 'Loading...'}</div>;
}