Stockio

Next.js Server vs Client Components: Where to Split

Learn how to split Next.js Server and Client Components, pass safe serializable props, and keep database access and secrets outside client import graphs.

By Sofia Marchetti

JavaScript Engineer ·

On this page

In the Next.js App Router, a component's location in the import graph matters as much as its JSX. A small interactive control can accidentally pull a large dependency tree into the client bundle, while moving all interaction to the server makes ordinary browser behavior difficult. The goal is a deliberate boundary, not a blanket rule against client code.

Design the data crossing first

Start with the data a browser interaction actually needs. A bookmark button may need a product identifier and an initial boolean, not a complete database record. Boundary design reduces coupling and limits accidental exposure. Server-rendered HTML and serialized props are both visible to the recipient.

Use JSON to TypeScript with a sanitized example of the intended prop object. Refine generated types into a small view model rather than exporting database entities wholesale. Generated types describe sample structure; they do not validate runtime data or identify confidential fields.

App Router pages and layouts are Server Components by default. They can read server resources directly when the runtime permits, without sending that access code to the browser. A Client Component is needed for state, effects, event handlers, and browser APIs. The distinction describes execution and composition, not whether the result initially appears as HTML.

Understand the client import graph

The 'use client' directive establishes a client entry boundary. Modules imported beneath that boundary become part of its client dependency graph, subject to bundling behavior. Adding the directive to a high-level layout can therefore move far more code than the one interactive control that motivated it.

Keep interactive islands small: filters, menus, editors, and cart controls can receive data from a server-rendered parent. The surrounding description, product specification, and related content can remain server components. This is ordinary composition, not a requirement to create a separate route for every interactive feature.

Read the Next.js Server and Client Components guide for the framework's supported patterns. Client Components can be prerendered on the server for the initial page load. Consequently, accessing window at module scope or unconditionally during render is still unsafe even in a client-marked module.

Do not import a server-only database module into a Client Component. Use the server-only package where appropriate to catch mistaken imports. Hiding a secret behind an unused branch is not a security strategy; the module graph and exposed outputs must enforce the boundary.

Pass a narrow view model

This example assumes getProduct is a server-only data-access function that applies any necessary authorization. The browser receives only the fields used by the interaction.

// app/products/[id]/page.tsx
import FavoriteButton from './FavoriteButton';
import { getProduct } from '@/lib/products';

export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProduct(id);
return (
  <main>
    <h1>{product.name}</h1>
    <FavoriteButton productId={product.id} />
  </main>
);
}

// app/products/[id]/FavoriteButton.tsx
'use client';
import { useState } from 'react';

export default function FavoriteButton({ productId }: { productId: string }) {
const [selected, setSelected] = useState(false);
return (
  <button
    type="button"
    aria-pressed={selected}
    data-product={productId}
    onClick={() => setSelected(value => !value)}
  >
    {selected ? 'Saved locally' : 'Save locally'}
  </button>
);
}

The button intentionally changes local state only. A durable favorite requires an authenticated mutation and a policy for reconciling the resulting server data. Local interactivity must not be mistaken for persistence merely because the component appears inside a server-rendered page.

React supports specific serializable values across this boundary; it is more nuanced than plain JSON, but arbitrary functions and class instances are not general prop transport. Consult the React use client reference before passing unusual values. A narrow plain-data contract is usually easiest to reason about.

Compose server content around client behavior

A Client Component can accept server-rendered content through a children slot. A server parent imports both pieces, renders the server content, and passes it into the client wrapper. The wrapper controls presentation without importing the server component module itself.

For example, a collapsible product details panel can own expanded state while its children contain server-fetched specifications. The toggle does not need to fetch those specifications again. Slot composition preserves the distinction between visual nesting and module dependency.

Context providers are client-side mechanisms. Place a provider as deep as practical rather than wrapping the entire document automatically. Server-rendered children can still be passed through that provider's slot, but Server Components do not read a client context during server execution.

Avoid building a generic boundary that accepts dozens of mode flags, callbacks, and records. A focused client wrapper with a few serializable props is easier to test than a universal component that coordinates the whole route. The existing headless architecture guide offers useful context for keeping content contracts independent of presentation concerns.

Verify execution, exposure, and navigation

Inspect the client output and network payload, not only the source directory names. Confirm that database packages and privileged helpers are absent from the browser graph. Check that props contain no internal notes, access tokens, or fields the current user is not authorized to see.

Then test a direct page load and a client-side navigation. Frameworks may use different delivery paths for those cases. Verify that initial rendering and later updates produce the same permissions and visible content. A component that works only after navigating from another page has an incomplete data or hydration contract.

Keep data access behind a server-side layer that performs authorization close to the resource. Server Components do not magically authorize database queries. Route parameters, cookies, and submitted identifiers still require validation against the current user's permissions. The Next.js data security guide is the appropriate reference for these boundaries.

Finally, document why each client entry exists: state, browser integration, or event handling. Use the React and frameworks collection for adjacent patterns. Explicit reasons make future reviews simpler than trying to minimize directive counts without understanding behavior.

Plan loading boundaries alongside component boundaries

Suppose a product page needs a fast product summary and slower warehouse availability. Keep the summary outside a Suspense boundary and place the asynchronous availability component inside it with a useful fallback. This lets the page reveal independent content without moving the database query into a client effect merely to display a spinner.

Suspense does not parallelize work that you already awaited sequentially in a parent. Start independent work together where appropriate, and keep dependent reads ordered when one result supplies the other's authorization or identifier. Review the actual request waterfall rather than assuming a component split guarantees faster delivery.

Also test an unavailable warehouse service. Loading feedback is temporary; a rejected operation needs a deliberate error boundary or safe fallback. The product purchase control must not interpret missing availability as permission to buy. Keep that decision authoritative on the server, even if the client displays an optimistic interface. These checks separate delivery improvements from business correctness and prevent a visually elegant boundary from concealing an unsafe assumption.

Key takeaways

  • Keep server access and authorization on the server, and pass narrow view models to interactive controls.
  • Treat 'use client' as an import-graph boundary, not a guarantee that code never renders on the server.
  • Test direct loads, navigation, serialized data exposure, and persistent mutations separately.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best TypeScript Learning Resources

Choose a practical path into TypeScript with official guides, books, interactive courses, and exercises for JavaScript learners, plus tips for practising types against realistic data.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

10 Best Free React Courses (Learn React for Free)

Choose a free React learning path that matches your experience, from official lessons and guided exercises to full projects. See what each resource teaches and what you should know first.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

10 Best React Frameworks

Compare ten React frameworks for websites, web apps, and full-stack projects. Learn which fits your rendering needs, hosting plans, team experience, and appetite for configuration.

Includes: JSON to TSDaniel KimDaniel Kim
JavaScript & Frameworks

Vue vs React: How to Choose for a New Project

Compare Vue 3 and React through state models, team workflows, full-stack options, and a practical prototype exercise that turns framework preferences into a defensible project decision.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

TypeScript Type Guards and Type Narrowing

Use control-flow analysis, discriminated unions, and honest custom type guards to turn uncertain values into dependable types without unsafe assertions.

Includes: JSON to TSSofia MarchettiSofia Marchetti
JavaScript & Frameworks

Typed Component Manifest for Your Design System

Build a typed component manifest for library catalogs, ownership checks, and documentation links, while separating generated TypeScript types from runtime validation.

Includes: JSON to TSDaniel KimDaniel Kim