Tutorials Logic, IN info@tutorialslogic.com

Next.js Server and Client Components: Decide Where Code Should Run

What Beginners Usually Misunderstand

The biggest mental shift in Next.js is that not every component needs to run in the browser.

Beginners often add "use client" too quickly because browser interactivity feels familiar, but that can throw away many of the framework benefits.

Professionals use server components by default and introduce client components only where state, effects, or browser APIs are truly needed.

This lesson matters because the server-versus-client boundary affects bundle size, security, and data flow all at once.

Many learners think server components are only for data fetching, but the idea is broader. If a piece of UI can be prepared without browser-only APIs or live client state, it can often remain on the server. That means less JavaScript is sent to the user.

A common beginner mistake is adding "use client" at the top of a parent component and accidentally turning a large part of the tree into client-side code when only one small button needed it.

  • Server components can read data and render markup without sending all that logic to the browser.
  • Client components are for interaction, local state, effects, and browser APIs.
  • The boundary should be as small as possible.

How Teams Draw The Boundary

Professionals often keep page shells, lists, and read-only data rendering on the server, then embed smaller client islands for search bars, filters, modals, or live controls. This creates a better balance between speed and interaction.

The question is not "can this be a client component?" The better question is "what is the minimum interactive surface that needs client behavior?" That framing usually produces cleaner code and smaller bundles.

  • Push interactivity down into narrow client leaf components.
  • Keep sensitive data access on the server whenever possible.
  • Review client boundaries during code review because they affect performance and security.

Decide Where A Component Should Execute

Components are Server Components by default in the App Router. They can read server-side data, use secrets, and send rendered output without adding their implementation to the browser bundle. Use them for pages, data fetching, and non-interactive presentation.

Add the use client directive only when a component needs state, effects, event handlers, or browser APIs. The directive creates a client boundary for that module and its imported dependencies. Keep the boundary close to the interactive control rather than marking an entire page or layout as client code.

Props crossing from server to client must be serializable. Pass small plain values, not database clients, class instances, functions, or secrets. Client Components may render Server Component output through composition, which can preserve server work while allowing an interactive shell.

  • Use Server Components as the default.
  • Add client boundaries only for browser interaction.
  • Keep client imports small and browser-safe.
  • Pass serializable minimal props.
  • Never expose server secrets through props.

Tradeoffs To Notice

Server components cannot use browser APIs, event handlers, or React hooks like useState and useEffect. Client components can, but they add bundle weight and hydration cost. Understanding this tradeoff is more important than memorizing the syntax.

The best developers treat the boundary as an architectural decision. It changes how fast the page loads, how easy the code is to test, and what logic stays private on the server.

  • A server component is often better for static or read-heavy views.
  • A client component is necessary for immediate browser interaction.
  • Too many client components usually means you are rebuilding a SPA inside a framework that can do more for you.

Shrink the Client Boundary Deliberately

Render a searchable catalog on the server and isolate only the filter controls as a Client Component. Pass serializable values rather than database objects or functions across the boundary.

Marking the page use client pulls descendants and dependencies into the browser bundle. Accessing secrets, filesystem APIs, or non-serializable values from the client boundary fails or leaks architecture.

Verification must use evidence that matches the concept. Compare bundle output before and after moving the boundary, inspect server HTML, and test interaction after hydration. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.

Bundle Cost, Streaming, Caching, And Security Boundaries

Inspect the production bundle when moving boundaries. A high-level client directive can pull formatting libraries, data helpers, and UI descendants into browser JavaScript. Split providers and interactive widgets narrowly, and avoid importing server-only modules from client code.

Server Components can stream through Suspense boundaries so useful content arrives before slower regions. Choose boundaries that match meaningful UI sections and avoid excessive nested loading flicker. Data cache behavior is separate from component type; define freshness and privacy for every fetch.

Authorization must occur in server data functions, Route Handlers, and Server Actions, not merely in a Server Component parent. Client Components remain untrusted. Test serialization, hydration, disabled JavaScript behavior, bundle size, and direct calls to mutation boundaries.

  • Measure bundle impact after moving boundaries.
  • Use Suspense around meaningful slow regions.
  • Define cache behavior independently from component type.
  • Authorize at every server data and mutation boundary.
  • Test hydration and direct endpoint access.

What Crosses the Component Boundary

The use client directive defines a module boundary, not a command to render only in the browser. Client Components can contribute HTML during the initial server render, then hydrate so event handlers and state work in the browser. Every module imported beneath that boundary joins the client module graph, which is why placing the directive on a large layout can ship far more JavaScript than expected.

Props passed from a Server Component to a Client Component must be serializable by React. Pass identifiers, text, numbers, booleans, arrays, and plain data objects rather than database connections, class instances, or arbitrary functions. Server Functions are the intentional exception when passed through supported action patterns. Sensitive values still must not be serialized merely because the transport accepts them.

Composition keeps boundaries narrow. A Client Component may accept Server Component content through children, allowing an interactive shell to display server-rendered work without importing that work into its client graph. Inspect the production bundle and React Server Component requests after moving a boundary; source code alone does not reveal the final payload cost.

  • Place use client in the smallest module that owns browser behavior.
  • Pass only safe serializable props across the boundary.
  • Use children composition when an interactive shell surrounds server content.
  • Keep authentication and authorization in server-side data and mutation functions.
  • Measure the client graph after changing imports or providers.

Component Boundary Examples

Server shell with client island

This split is one of the most practical patterns to understand early.

Server shell with client island
export default async function ProductsPage() {
  const products = await getProducts();

  return (
    <section>
      <h1>Products</h1>
      <ProductFilters />
      <ProductList products={products} />
    </section>
  );
}

// ProductFilters.tsx
'use client';

export function ProductFilters() {
  return <button>Open filters</button>;
}
  • The page and list stay on the server.
  • Only the interactive filter control becomes client code.
  • This keeps the boundary small and easier to reason about.

Shrink the Client Boundary Deliberately example

Shrink the Client Boundary Deliberately example
export default async function CatalogPage() {
  const products = await listProducts();
  return <Catalog products={products} filters={<ProductFilters />} />;
}

Server page with a small client control

Only the counter code needs to run in the browser.

Server page with a small client control
export default async function ProductPage({ params }) {
  const product = await getProduct((await params).id);
  return <article>
    <h1>{product.name}</h1>
    <p>{product.description}</p>
    <QuantityPicker productId={product.id} />
  </article>;
}
  • getProduct stays server-side.
  • QuantityPicker receives only safe serializable values.
  • Keep the product description available without hydration.

Focused Client Component boundary

Browser state stays inside the smallest interactive module.

Focused Client Component boundary
\"use client\";

import { useState } from \"react\";

export function QuantityPicker({ productId }) {
  const [quantity, setQuantity] = useState(1);
  return <label>
    Quantity
    <input type=\"number\" min=\"1\" value={quantity}
      onChange={event => setQuantity(Number(event.target.value))} />
  </label>;
}
  • Validate quantity again on the server mutation.
  • Do not import server data modules here.
  • Measure accessibility and bundle cost.
Before you move on

Next.js Server and Client Components: Decide Where Code Should Run Mastery Check

1 checks
  • When a component needs "use client".

Next.js Questions Learners Ask

Yes. That is a normal pattern. The server component can provide data and structure while the child client component handles interaction.

No. Use them when the browser really needs to manage state, effects, or user interaction. The goal is not zero client code; the goal is intentional client code.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.