Component Development
Next.js App Router defaults to Server Components. Understanding when and how to use each component type is essential for building performant applications.
Server Components vs Client Components
| Aspect | Server Component | Client Component |
|---|---|---|
| Directive | None (default) | "use client" |
| Runs on | Server only | Server (SSR) + Browser |
Can use async/await | Yes | No |
Can use hooks (useState) | No | Yes |
| Can use event handlers | No | Yes |
| Can access backend directly | Yes | No |
| Bundle size impact | Zero JS sent | Adds to JS bundle |
When to Use "use client"
Add the directive only when your component needs:
- React hooks —
useState,useEffect,useRef, etc. - Event handlers —
onClick,onChange,onSubmit - Browser APIs —
window,localStorage,IntersectionObserver - Third-party libraries that depend on client-side state
"use client";
import { useState } from "react";
export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && onSearch(query)}
placeholder="Search..."
className="rounded border px-3 py-2"
/>
);
}
Data Fetching in Server Components
Server Components can fetch data directly without useEffect:
// app/users/page.tsx (Server Component)
async function getUsers() {
const res = await fetch("https://api.example.com/users", {
next: { revalidate: 60 }, // Cache for 60 seconds
});
return res.json();
}
export default async function UsersPage() {
const users = await getUsers();
return (
<ul>
{users.map((user: { id: string; name: string }) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Component Composition Pattern
Keep the interactive boundary small by composing server and client components:
// app/dashboard/page.tsx (Server Component)
import { DashboardChart } from "./dashboard-chart"; // Client
import { getMetrics } from "@/lib/data";
export default async function DashboardPage() {
const metrics = await getMetrics();
return (
<div>
<h1>Dashboard</h1>
<p>Total users: {metrics.totalUsers}</p>
{/* Pass server-fetched data as props to client component */}
<DashboardChart data={metrics.chartData} />
</div>
);
}
Reusable Component Patterns
Create flexible components with variants using class-variance-authority:
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva("rounded-full px-2 py-1 text-xs font-medium", {
variants: {
variant: {
default: "bg-gray-100 text-gray-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
},
},
defaultVariants: { variant: "default" },
});
interface BadgeProps extends VariantProps<typeof badgeVariants> {
children: React.ReactNode;
className?: string;
}
export function Badge({ variant, className, children }: BadgeProps) {
return <span className={cn(badgeVariants({ variant }), className)}>{children}</span>;
}
Prefer Server Components for anything that does not require interactivity. This reduces the JavaScript shipped to the browser and improves performance.