ThanhDev
  • Articles
  • Categories
  • Tags
  • About

ThanhDev

Frontend engineering notes on React, Next.js, TypeScript and software architecture.

  • Articles
  • Categories
  • Tags
  • About
  • GitHub
  • LinkedIn
  • Website
  • Email

© 2026 ThanhDev. Built with Next.js, Prisma and PostgreSQL.

  1. Home
  2. Articles
  3. Next.js App Router: Server Components in Practice
Next.js

Next.js App Router: Server Components in Practice

Where the server/client boundary actually belongs, and how to stop accidentally shipping your whole application to the browser.

T

ThanhDev

Aug 30, 2026•2 min read•882 views

The App Router's default is the part people miss: every component is a Server Component until you say otherwise. That default is doing a lot of work for you, and one misplaced directive can undo it.

The boundary is a tree, not a file

"use client" does not mark one file as client-side. It marks an entry point. Everything that file imports becomes part of the client bundle too.

tsx
// app/dashboard/page.tsx — Server Component
import { Chart } from "./chart";      // client
import { formatCurrency } from "@/lib/format"; // pulled into the client bundle

So the goal is to push the directive as far down the tree as possible.

A concrete refactor

Here is the version that ships too much:

tsx
"use client";
 
import { useState } from "react";
 
export function PostList({ posts }: { posts: Post[] }) {
  const [query, setQuery] = useState("");
  const visible = posts.filter((p) => p.title.includes(query));
 
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      {visible.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </>
  );
}

PostCard is now a Client Component whether it needs to be or not. Move the state into the smallest possible island and let the server render the rest:

tsx
// server
export function PostList({ posts }: { posts: Post[] }) {
  return (
    <>
      <SearchInput />
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </>
  );
}

SearchInput writes the query to the URL, the page reads searchParams, and filtering happens on the server against the database instead of against an array you had to send over the wire.

Rules that have held up

  • Data fetching belongs in Server Components. Always.
  • useState for UI state is a fine reason to go client. useState for server state usually is not.
  • Passing a Server Component as children to a Client Component keeps it on the server.
  • Anything you pass across the boundary must be serializable — no functions, no class instances, no Date methods you rely on.
tsx
// This works: Sidebar stays a Server Component
<ClientShell>
  <Sidebar />
</ClientShell>

The measurement that matters

Run a production build and read the route table. If a mostly-static page reports a large First Load JS, you have a boundary in the wrong place. That number is the honest scoreboard.

  • #Next.js
  • #React
  • #Server Components
Share
PreviousTypeScript Discriminated Unions for Safer UI StateNextUnderstanding React Rendering

Related articles

  • Next.js

    Next.js Hydration

    Next.js Hydration — hiểu đúng để tối ưu SSR trong dự án thực tế

    • #Next.js
    Sep 10, 2026•13 min read•1 views
  • React

    TanStack Query or Server Components?

    Both fetch data, but they solve different problems. A decision guide instead of a turf war.

    • #React
    • #TanStack Query
    • #Server Components
    Jul 30, 2026•2 min read•687 views
  • React

    Understanding React Rendering

    A practical explanation of how React rendering actually works under the hood, and why most re-render bugs are really state placement bugs.

    • #React
    • #Performance
    Sep 5, 2026•2 min read•49 views