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

AspectServer ComponentClient Component
DirectiveNone (default)"use client"
Runs onServer onlyServer (SSR) + Browser
Can use async/awaitYesNo
Can use hooks (useState)NoYes
Can use event handlersNoYes
Can access backend directlyYesNo
Bundle size impactZero JS sentAdds to JS bundle

When to Use "use client"

Add the directive only when your component needs:

  • React hooksuseState, useEffect, useRef, etc.
  • Event handlersonClick, onChange, onSubmit
  • Browser APIswindow, 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.