Files
greedychipmunk__agent-skills/nextjs-developer/resources/server-components.md
2026-01-22 10:18:31 -05:00

7.5 KiB

Server Components vs Client Components

Overview

Next.js App Router uses React Server Components by default. Understanding when to use Server vs Client Components is crucial for building performant applications.


Server Components (Default)

Server Components render on the server and send HTML to the client.

Benefits

  • Zero JavaScript: No JS bundle sent to client for the component
  • Direct backend access: Query databases, access file system
  • Improved security: Keep sensitive data on server
  • Caching: Results can be cached and reused
  • Better SEO: Full HTML available for crawlers

Capabilities

  • Fetch data directly
  • Access backend resources
  • Keep sensitive information server-side
  • Import large dependencies without client impact

Limitations

  • Cannot use hooks (useState, useEffect)
  • Cannot use browser APIs
  • Cannot add event handlers (onClick, onChange)
  • Cannot use Context providers

Example

// app/posts/page.tsx (Server Component by default)
import { db } from "@/lib/db"

export default async function PostsPage() {
  // Direct database access
  const posts = await db.post.findMany({
    include: { author: true },
  })

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
        </li>
      ))}
    </ul>
  )
}

Client Components

Client Components are pre-rendered on the server and hydrated on the client for interactivity.

When to Use

  • Interactivity: onClick, onChange handlers
  • State: useState, useReducer
  • Effects: useEffect, useLayoutEffect
  • Browser APIs: window, document, localStorage
  • Custom hooks: Hooks that depend on state/effects
  • React Class components: If using legacy patterns

Declaration

"use client" // Add at the top of the file

import { useState } from "react"

export function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Composition Patterns

Pattern 1: Server Component with Client Children

// app/dashboard/page.tsx (Server Component)
import { db } from "@/lib/db"
import { InteractiveChart } from "./chart"

export default async function Dashboard() {
  const data = await db.analytics.findMany()

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Pass server data to client component */}
      <InteractiveChart data={data} />
    </div>
  )
}
// app/dashboard/chart.tsx (Client Component)
"use client"

import { useState } from "react"

export function InteractiveChart({ data }: { data: DataPoint[] }) {
  const [selectedRange, setSelectedRange] = useState("week")

  return (
    <div>
      <select
        value={selectedRange}
        onChange={(e) => setSelectedRange(e.target.value)}
      >
        <option value="day">Day</option>
        <option value="week">Week</option>
        <option value="month">Month</option>
      </select>
      {/* Render chart with data */}
    </div>
  )
}

Pattern 2: Client Component Wrapping Server Children

// app/page.tsx (Server Component)
import { Modal } from "@/components/modal"
import { ServerContent } from "./server-content"

export default function Page() {
  return (
    <Modal>
      {/* Server Component passed as children */}
      <ServerContent />
    </Modal>
  )
}
// components/modal.tsx (Client Component)
"use client"

import { useState } from "react"

export function Modal({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open</button>
      {isOpen && (
        <div className="modal">
          {children}
          <button onClick={() => setIsOpen(false)}>Close</button>
        </div>
      )}
    </>
  )
}

Decision Guide

Need Component Type
Fetch data Server
Access backend resources Server
Keep sensitive info on server Server
Large dependencies Server
Add interactivity (onClick) Client
Use state (useState) Client
Use effects (useEffect) Client
Use browser APIs Client
Use custom hooks with state Client

Common Patterns

Passing Server Data to Client Components

// Server Component
export default async function Page() {
  const user = await getUser()

  // Serialize and pass to client
  return <UserProfile user={user} />
}

// Client Component
"use client"

export function UserProfile({ user }: { user: User }) {
  const [isEditing, setIsEditing] = useState(false)
  // user data is already fetched, just handle UI state
}

Moving Client Components Down

// BAD: Entire page is client component
"use client"

export default function Page() {
  const [search, setSearch] = useState("")
  const posts = usePosts() // Client-side fetching

  return (
    <div>
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      <PostList posts={posts} />
    </div>
  )
}
// GOOD: Only SearchBar is client component
// app/posts/page.tsx (Server Component)
export default async function Page() {
  const posts = await getPosts() // Server-side fetching

  return (
    <div>
      <SearchBar /> {/* Client component */}
      <PostList posts={posts} /> {/* Server component */}
    </div>
  )
}

// app/posts/search-bar.tsx
"use client"

export function SearchBar() {
  const [search, setSearch] = useState("")
  // Handle search with URL params or server action
}

Context Providers

// app/providers.tsx
"use client"

import { ThemeProvider } from "next-themes"

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system">
      {children}
    </ThemeProvider>
  )
}

// app/layout.tsx (Server Component)
import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Third-Party Libraries

Many third-party components need "use client" because they use hooks.

Wrapping Client-Only Libraries

// components/carousel.tsx
"use client"

import { Carousel as LibCarousel } from "some-carousel-lib"

export function Carousel(props: CarouselProps) {
  return <LibCarousel {...props} />
}

Using in Server Components

// app/page.tsx (Server Component)
import { Carousel } from "@/components/carousel"

export default async function Page() {
  const images = await getImages()

  return (
    <div>
      <h1>Gallery</h1>
      <Carousel images={images} />
    </div>
  )
}

Serialization Rules

When passing props from Server to Client Components, data must be serializable:

Supported Types

  • Primitives (string, number, boolean, null, undefined)
  • Arrays and objects containing serializable values
  • Date (serialized to string)
  • Map, Set (serialized)
  • TypedArrays, ArrayBuffer

Not Supported

  • Functions
  • Classes (instances)
  • Symbols
  • React elements (except as children)
// This works
<ClientComponent
  data={{ name: "John", count: 42 }}
  items={["a", "b", "c"]}
  date={new Date()}
/>

// This does NOT work
<ClientComponent
  onClick={() => console.log("clicked")} // Function
  user={new User("John")} // Class instance
/>

Last Updated: January 2026