CodeWithSabir
HomeAIDevOpsNext.jsMobile DevelopmentWeb Development
CodeWithSabir
  • Home
  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development
  • About
  • Contact
CodeWithSabir

In-depth articles, tutorials, and guides on web development, React, Next.js, AI, and modern programming practices.

Topics

  • AI
  • DevOps
  • Next.js
  • Mobile Development
  • Web Development

Company

  • About
  • Contact
  • Privacy Policy
  • Terms

© 2026 CodeWithSabir. All rights reserved.

Built with SabirSoft.com

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

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

A clear explanation of React Server Components in Next.js — what problem they solve, how they differ from Client Components, and the patterns that make them powerful.

Sabir KhaloufiSabir KhaloufiFebruary 20, 20264 min read

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

tsx
// 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
tsx
'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.

tsx
// 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:

tsx
// 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>
  )
}
tsx
// 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:

tsx
// 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:

tsx
// 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
#next.js#server components#react#app router#performance
Share:
Sabir Khaloufi — author photo

Written by

Sabir Khaloufi

Full-stack developer and tech blogger sharing in-depth tutorials on React, Next.js, AI, and modern web development.

On this page

  • The Problem They Solve
  • Server Components vs. Client Components
  • A Concrete Example
  • When to Use Client Components
  • The Composition Pattern
  • Async Rendering and Suspense
  • Common Mistakes
  • Key Takeaways

Related Articles

Next.js

How to Build a Fullstack App with Next.js 15: Complete Guide

A hands-on guide to building a fullstack application with Next.js 15 — covering App Router, Server Actions, database integration, authentication, and deployment.

Sabir KhaloufiFebruary 25, 20265 min read
Next.js

Authentication in Next.js with NextAuth.js v5: The Complete Setup

Learn how to implement authentication in Next.js using NextAuth.js v5 — covering credentials, OAuth providers, JWT sessions, protected routes, and role-based access control.

Sabir KhaloufiFebruary 15, 20264 min read
Next.js

Next.js Performance Optimization: 10 Techniques That Make a Real Difference

Practical Next.js performance optimizations covering image optimization, caching strategies, bundle analysis, lazy loading, and Core Web Vitals improvements that affect Google rankings.

Sabir KhaloufiFebruary 10, 20264 min read