Back to roadmaps nextjs Course

Hybrid Architecture: Server and Client Components

Next.js uses a hybrid rendering architecture. By combining React Server Components (RSC) and Client Components, you can build highly interactive pages without sacrificing load speed or SEO value.


1. The Component Boundary Concept

In Next.js App Router, components inside the app directory are divided by import boundaries:

Server Component (Default)
  └── imports another Server Component (Runs on server)
  └── imports a Client Component (Boundary Created)
        └── Client Component (Runs on browser, hydrates)

Once a file contains the "use client" directive at the top, it marks the entry point of the Client Component boundary. That file and all files imported into it are compiled as client-side code.


2. Coordination Guidelines

For optimal performance, keep interactive client boundaries as small as possible:

  • Static Layout Frame: Keep main layouts, database queries, and headers as Server Components.
  • Leaf-Level Interactivity: Wrap interactive nodes (like search inputs, toggle buttons, or modal popups) into dedicated client files.

3. Composition Patterns and Rules

To prevent server-only code from leaking into the browser bundle, follow these composition rules:

Rule 1: You Cannot Import Server Components into Client Components

If you attempt to import a Server Component directly inside a Client Component, Next.js compiles the Server Component as client-side code anyway, breaking server-only features (such as database calls or secure keys).

// WARNING: This direct import pattern is invalid!
"use client";

import MyServerComponent from "./MyServerComponent"; // Breaks server-only logic

Rule 2: Passing Server Components as Children

To render a Server Component inside a Client Component, pass the Server Component down as a React node child parameter. This ensures the Server Component compiles on the server first:

// src/app/components/ClientWrapper.tsx
"use client";

import { useState } from "react";

export function ClientWrapper({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle Details</button>
      {isOpen && children}
    </div>
  );
}
// src/app/page.tsx (Server Component)
import { ClientWrapper } from "./components/ClientWrapper";
import MyServerComponent from "./components/MyServerComponent";

export default function Home() {
  return (
    <ClientWrapper>
      {/* Renders correctly as a Server Component */}
      <MyServerComponent />
    </ClientWrapper>
  );
}
Published on Last updated: