Next.js Server Components — How They Work

Sanjeev SharmaSanjeev Sharma
4 min read

Advertisement

Next.js Server Components — How They Work

Server Components represent a paradigm shift in React development. They allow components to run exclusively on the server, sending only HTML to the browser.

What Are Server Components?

Server Components execute entirely on the server. They can:

  • Access databases directly
  • Keep sensitive API keys server-side
  • Fetch data without creating API layers
  • Render and send HTML to the client
// app/posts/page.tsx (Server Component by default)
async function getPosts() {
  const posts = await db.posts.findMany()
  return posts
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  )
}

Client Components vs Server Components

Server Components:

  • Async/await support
  • Database access
  • No browser APIs
  • Reduced bundle size
  • Better security

Client Components (marked with 'use client'):

  • Interactive features
  • Browser APIs
  • State and hooks
  • Event listeners

When to Use Server Components

Use Server Components for:

  • Pages that fetch data
  • Reading from databases
  • Protecting sensitive information
  • Large dependencies you don't want in the client bundle
// app/dashboard/page.tsx
import { auth } from '@/lib/auth'

export default async function DashboardPage() {
  const session = await auth()

  if (!session) {
    redirect('/login')
  }

  const userData = await db.users.findUnique({
    where: { id: session.userId }
  })

  return (
    <div>
      <h1>Welcome, {userData.name}</h1>
      <Dashboard user={userData} />
    </div>
  )
}

Rendering Server Components

Server Components render on the server, generating HTML sent to the browser:

// Pure Server Component - no JS on client
export default async function ServerCounter() {
  const count = await getCountFromDatabase()
  return <div>Count: {count}</div>
}

The browser receives pre-rendered HTML without JavaScript.

Mixing Server and Client Components

Nest Client Components within Server Components:

// app/dashboard/page.tsx (Server Component)
import { ClientChart } from '@/components/chart'

export default async function DashboardPage() {
  const data = await fetchAnalytics()

  return (
    <div>
      <h1>Dashboard</h1>
      <ClientChart data={data} />
    </div>
  )
}
// components/chart.tsx
'use client'

import { useState } from 'react'

export function ClientChart({ data }) {
  const [selectedMetric, setSelectedMetric] = useState('users')

  return (
    <div>
      <select onChange={(e) => setSelectedMetric(e.target.value)}>
        <option>users</option>
        <option>revenue</option>
      </select>
      <Chart data={data} metric={selectedMetric} />
    </div>
  )
}

Data Fetching in Server Components

// Fetch at the component level
export default async function UserProfile({ userId }) {
  const user = await fetch(`/api/users/${userId}`, {
    next: { revalidate: 60 }
  }).then(r => r.json())

  return <div>{user.name}</div>
}

The revalidate option controls cache duration.

Performance Benefits

Server Components provide significant advantages:

  • No unnecessary JavaScript shipped to clients
  • Database queries run server-side, faster
  • Sensitive data stays on the server
  • Smaller initial page load

Error Handling

Server Components can throw errors that Next.js catches:

export default async function DataDisplay() {
  try {
    const data = await riskyDataFetch()
    return <div>{data}</div>
  } catch (error) {
    return <div>Error loading data</div>
  }
}

For unhandled errors, Next.js renders the nearest error.tsx boundary.

Server Component Patterns

Pattern 1: Loading Data

const data = await fetchData()
return <Component data={data} />

Pattern 2: Conditional Rendering

const user = await getUser()
if (!user) return <NotFound />
return <UserPage user={user} />

Pattern 3: Server-side Search

async function SearchResults({ query }) {
  const results = await db.posts.findMany({
    where: { title: { contains: query } }
  })
  return <div>{results.map(r => <Item key={r.id} item={r} />)}</div>
}

Common Mistakes

Mistake 1: Using hooks in Server Components

// WRONG
export default async function Page() {
  const [count, setCount] = useState(0) // ERROR
  return <div>{count}</div>
}

Mistake 2: Passing non-serializable data to Client Components

// WRONG
export default async function Page() {
  const date = new Date() // Not serializable
  return <ClientComponent date={date} />
}

// CORRECT
export default async function Page() {
  const date = new Date().toISOString() // Serializable string
  return <ClientComponent date={date} />
}

FAQ

Q: Can Server Components use context? A: No, but you can use context in Client Components nested within Server Components.

Q: Should all components be Server Components? A: By default, yes. Only use 'use client' when you need interactivity or hooks.

Q: How do I pass functions to Client Components from Server Components? A: You can't pass functions directly. Use Server Actions instead, which are callable server functions.


Understanding Server Components is crucial for building efficient Next.js applications.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro