Follow Us

CodeWithSabir

  • Contact Us
  • Privacy Policy
  • About
  • Terms & Conditions

All Rights Reserved © 2026

  • Light
  • Dark
Next.js

Next.js Server Components Explained: What They Are and Why They Matter

Sabir Soft
Sabir Lkhaloufi
  • February 20, 2026
  • 4 min read

Next.js Server Components Explained: What They Are and Why They Matter

Server Components are the most significant architectural change to React in years, and they're the foundation of the Next.js App Router. If you've moved from the Pages Router to the App Router, you've been using them — but do you actually understand why they behave differently?

This article explains Server Components from first principles so you understand when to use them, when not to, and what patterns to follow.

The Problem They Solve

In traditional React (before Server Components), every component ran in the browser. You'd fetch data in a useEffect, show a loading state, then render the content. The result:

  1. Browser downloads the JavaScript bundle
  2. React hydrates and renders the loading state
  3. Component mounts, useEffect fires
  4. Data fetch begins (going back to the server you just left)
  5. Data returns, state updates, re-render

This is called a "client-server waterfall." The user stares at a loading spinner while the browser makes a round trip to the server for data that the server already had.

Server Components eliminate this by running the rendering on the server. The component fetches data right there, on the server where the data lives, and sends the already-rendered HTML to the browser.

Server Components vs. Client Components

The difference isn't about SSR (Server-Side Rendering). Both Server and Client Components can be server-rendered. The real difference:

Server ComponentClient Component
Runs onServer onlyServer (initial) + Browser
Can useasync/await, direct DB accessuseState, useEffect, browser APIs
Sends to browserHTML + dataHTML + JavaScript bundle
Re-rendersNever in browserOn state/prop change

Server Components never send their JavaScript to the browser. They don't add to your bundle size at all.

A Concrete Example

// Server Component (default in App Router — no 'use client')
// app/posts/page.tsx
 
import { db } from '@/lib/db'
 
export default async function PostsPage() {
  // Direct database call — no API route needed
  const posts = await db.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    take: 10,
  })
 
  return (
    <div>
      <h1>Latest Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

This component:

  • Runs on the server during the request
  • Directly queries the database
  • Never ships any JavaScript to the browser
  • The rendered HTML is what the browser receives

Compare to the Pages Router equivalent that required getServerSideProps — now the data fetching is just part of the component itself.

When to Use Client Components

You need 'use client' when your component:

  • Uses useState or useReducer
  • Uses useEffect or any lifecycle hook
  • Uses browser-only APIs (window, document, localStorage)
  • Uses event listeners (onClick, onChange, etc.)
  • Uses third-party libraries that require the browser
'use client'
 
import { useState } from 'react'
 
export default function LikeButton({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount)
  const [liked, setLiked] = useState(false)
 
  function handleLike() {
    setCount(c => liked ? c - 1 : c + 1)
    setLiked(l => !l)
  }
 
  return (
    <button onClick={handleLike} className={liked ? 'text-red-500' : ''}>
      ♥ {count}
    </button>
  )
}

The Composition Pattern

The key insight for using Server and Client Components together: Client Components can't import Server Components, but Server Components can render Client Components as children.

// app/post/[slug]/page.tsx — Server Component
import { getPostBySlug } from '@/lib/posts'
import LikeButton from '@/components/LikeButton'  // Client Component
import CommentSection from '@/components/CommentSection'  // Client Component
 
export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)  // Server-side data fetch
 
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      
      {/* Pass server-fetched data as props to Client Components */}
      <LikeButton initialCount={post.likes} postId={post.id} />
      <CommentSection postId={post.id} initialComments={post.comments} />
    </article>
  )
}

The Server Component fetches all data, then passes it as props to Client Components. The Client Components handle interactivity. The Server Component itself sends zero JavaScript.

Async Rendering and Suspense

Server Components support async/await natively. Combine them with Suspense for streaming:

// app/dashboard/page.tsx
import { Suspense } from 'react'
import RecentPosts from '@/components/RecentPosts'
import Analytics from '@/components/Analytics'
import PostSkeleton from '@/components/PostSkeleton'
 
export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-6">
      {/* These load independently — slow one doesn't block fast one */}
      <Suspense fallback={<PostSkeleton />}>
        <RecentPosts />
      </Suspense>
      
      <Suspense fallback={<div>Loading analytics...</div>}>
        <Analytics />
      </Suspense>
    </div>
  )
}
// components/RecentPosts.tsx — Server Component
async function RecentPosts() {
  // Even if this takes 2 seconds, Analytics loads independently
  const posts = await db.post.findMany({ take: 5, orderBy: { createdAt: 'desc' } })
  
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

Without Suspense, the slower component would block the entire page. With Suspense, each section streams independently.

Common Mistakes

1. Adding 'use client' to everything. This defeats the purpose. Only add it when you actually need browser APIs or interactivity. A navigation menu that opens on click needs 'use client'. A list of blog posts rendered from a database doesn't.

2. Prop-drilling through Client Components to Server Components. You can't import a Server Component inside a Client Component. Instead, pass Server Components as children to Client Components:

// WRONG
'use client'
import ServerDataList from './ServerDataList' // This won't work as expected
 
// RIGHT — pass as children
// Server Component
<ClientWrapper>
  <ServerDataList />  {/* Server Component passed as children prop */}
</ClientWrapper>

3. Fetching in Server Components that aren't at the top level. This can cause sequential waterfalls. Use Promise.all to parallelize:

// SLOW — sequential
const user = await getUser(id)
const posts = await getPostsByUser(user.id)  // Waits for user first
 
// FAST — parallel
const [user, posts] = await Promise.all([
  getUser(id),
  getPostsByUser(id),  // Starts immediately
])

4. Mixing async with useState. Server Components are async. Client Components aren't. If your component needs to be async AND interactive, fetch in a Server Component parent and pass data as props to a Client Component.

Key Takeaways

  • Server Components run only on the server — they never ship JavaScript to the browser
  • Use Server Components by default; only add 'use client' when you need interactivity
  • Server Components can directly access databases, file systems, and secrets
  • Combine Suspense with Server Components for progressive streaming
  • Pass fetched data from Server Components to Client Components as props
  • Parallel data fetching with Promise.all prevents server-side waterfalls
Popular Blogs
Claude AI vs ChatGPT: An Honest Comparison for Developers
  • April 28, 2026
AI Tools Every Developer Should Be Using in 2026
  • April 20, 2026
Using the Claude API in Real Projects: A Practical Developer Guide
  • April 15, 2026
Prompt Engineering for Developers: Write Prompts That Actually Work
  • April 10, 2026
Categories
AIDevOpsNext.jsMobile DevelopmentWeb Development

Related Posts

Next.js
How to Build a Fullstack App with Next.js 15: Complete Guide
Sabir Khaloufi·Feb 25, 2026
Next.js
Authentication in Next.js with NextAuth.js v5: The Complete Setup
Sabir Khaloufi·Feb 15, 2026
Next.js
Next.js Performance Optimization: 10 Techniques That Make a Real Difference
Sabir Khaloufi·Feb 10, 2026