Back to blog

React Server Components vs Client Components

January 15, 20252 min read
ReactNext.jsRSC

Two Component Models

  • **Server Components**: rendered on the server, no `useState` or effects, but you can directly `await` a database query or a `fetch` without going through a `useEffect`.
  • **Client Components**: hydrated on the browser side, can use React hooks and respond to events (`onClick`, `onChange`, etc.).

By default in the App Router, every component is a Server Component. You switch to a Client Component only by adding `"use client"` at the top of the file.

When to Choose One or the Other

The question to ask isn’t “which is better” but “what does this component actually need to do on the browser side.” A Client Component becomes necessary as soon as you need:

  • **State and interactivity**: `useState`, `useEffect`, client-side form handling, JS-driven animations.
  • **Browser APIs**: `localStorage`, `IntersectionObserver`, geolocation, etc.
  • **Libraries that depend on the DOM**: most chart libraries, drag-and-drop, or advanced animation tools.

Everything else—displaying data, composing layouts, formatting text—can remain a Server Component, and that’s almost always preferable: less JavaScript sent to the browser, faster rendering, no flash of unstyled content.

The Most Common Mistake

The classic trap is placing `"use client"` too high in the component tree. A single interactive button on a page is often enough to accidentally flip the entire page (and everything it imports) to the client side. The best practice is to isolate the interactive part in its own small component and keep everything else—layout, content, data fetching—as Server Components around it.

Example with App Router

A default server component that fetches data and delegates interactivity:

// app/page.tsx
import Posts from "./posts"

export default async function Home() {
  const posts = await getPosts()
  return <Posts posts={posts} />
}

And a minimal Client Component, only for the part that truly needs interactivity:

"use client"
import { useState } from "react"

export function Posts({ posts }: { posts: Post[] }) {
  const [query, setQuery] = useState("")
  const filtered = posts.filter((p) =>
    p.title.toLowerCase().includes(query.toLowerCase())
  )

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        className="mb-4 rounded-md border border-border px-3 py-2 text-sm"
        placeholder="Search an article…"
      />
      <ul className="space-y-2">
        {filtered.map((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </>
  )
}

Conclusion

Separating Server and Client components properly reduces the JavaScript sent and improves performance. The reflex to keep: default to Server Components, and only move to Client Components for the precise portion that truly needs it.