Skip to content

TopicsIntermediate

React Server Components vs Client Components

React Server Components vs Client Components: what runs where, when to add "use client", and how to keep bundles small.

Mohamed ElshahawyPublished Oct 2026
2 min read

What you'll learn

  • Server Components by default for data and static UI
  • Client Components for interactivity, browser APIs, and local state
  • Pass serializable props across the boundary
  • Checklist before marking a file client

Direct answer: keep components on the server until they need state, effects, or browser APIs. Mark that leaf (or a small island) with "use client". Do not make the whole app a Client Component tree by default.

Deep rendering modes: React Rendering Strategies. Decision of SSR/SSG/ISR/CSR: SSR, SSG, ISR, or CSR.

When to use Server Components

  • Fetching data close to the source without shipping a client waterfalls of fetch code
  • Large static or mostly static UI
  • Keeping secrets and heavy dependencies off the client bundle

When to use Client Components

  • useState, useReducer, useEffect
  • Event handlers (onClick, forms with client validation UX)
  • Browser-only APIs (window, localStorage, observers)

When not to force either

  • A pure presentational component with no hooks can stay a Server Component and be imported by both
  • Do not wrap the root layout in "use client" just to use one interactive widget

Practical example

TSX
// app/posts/page.tsx (Server Component)
import { LikeButton } from './like-button'

export default async function PostsPage() {
  const posts = await db.posts.list()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <LikeButton postId={post.id} />
        </li>
      ))}
    </ul>
  )
}
TSX
// app/posts/like-button.tsx
'use client'

import { useState } from 'react'

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false)
  return (
    <button type="button" onClick={() => setLiked(true)}>
      {liked ? 'Liked' : 'Like'} {postId}
    </button>
  )
}

Comparison

ConcernServer ComponentClient Component
Runs whereServer (per request/build)Browser (+ SSR hydrate)
HooksNoYes
Bundle costNot shipped as JS to clientShipped
Secrets / DBOKNever

Common mistakes

  • "use client" at the top of every file
  • Passing non-serializable props (functions, class instances) across the boundary
  • Fetching the same data again on the client after the server already had it

Checklist

  • Does this file need state or effects? If no, keep it server.
  • Is the client boundary as low in the tree as possible?
  • Are props across the boundary serializable?
  • Did you measure bundle impact after adding a client island?

Final recommendation

Default to Server Components. Push "use client" to interactive leaves. That keeps HTML fast and JavaScript smaller.