Technology · Next.js

Next.js Server Components

A practical guide to server components, the use client directive, and rendering boundaries.

TL;DR
  1. 01Server components run on the server and send HTML to the browser.
  2. 02Use client components with the "use client" directive for interactivity.
  3. 03Mix both types to optimize performance and security.

Server Components Basics

  • Server components are the default in the App Router.
    // app/page.tsx - server component by default
    export default async function Page() {
      const data = await fetch('https://api.example.com/data');
      return <div>{data}</div>;
    }
  • Fetch data directly inside the component without useEffect.
  • Access databases and secrets safely since code never reaches the browser.
  • Return HTML that is sent to the client for instant page load.
  • Cannot use hooks like useState or event listeners in server components.

Client Components

  • Add "use client" at the top of a file to make it a client component.
    "use client";
    import { useState } from "react";
    export default function Counter() {
      const [count, setCount] = useState(0);
      return <button onClick={() => setCount(count + 1)}>{count}</button>;
    }
  • Client components run in the browser and can use all React hooks.
  • Use them only for interactive features that need state or event handlers.
  • They still benefit from server-side rendering, just with client hydration.
  • Keep client components small to minimize the JavaScript bundle.

Mixing Server and Client

  • Pass server component data to a client component as a prop.
    // Server component
    export default async function Page() {
      const data = await fetchData();
      return <ClientComponent data={data} />;
    }
    
    // Client component
    "use client";
    export function ClientComponent({ data }) {
      const [count, setCount] = useState(0);
      return <div>{data}</div>;
    }
  • Server components can wrap client components and pass props down.
  • This pattern keeps sensitive code on the server and interactivity on the client.
  • Use server components as high-level wrappers for data fetching.
  • Pass serializable values only — functions and class instances cannot cross the boundary.
    // OK: strings, numbers, plain objects, arrays
    <ClientComponent name={user.name} count={42} items={["a", "b"]} />
    
    // Not OK: functions defined in server components
    // <ClientComponent onClick={serverFn} /> // will error

Server-Only Code

  • Use the "use server" directive to mark functions that run only on the server.
    "use server";
    export async function deletePost(id: number) {
      await db.delete('posts', id);
    }
  • Call server functions from client components like normal functions.
    "use client";
    import { deletePost } from "./actions";
    export default function Post({ id }) {
      return <button onClick={() => deletePost(id)}>Delete</button>;
    }
  • Server functions are type-safe and automatically serialized over the network.
  • Use them for mutations and sensitive operations that should not expose logic.
  • Import server-only package to prevent accidental client-side imports.
    import "server-only"; // throws a build error if imported in a client component
    
    export async function getSecretData() {
      return await db.query("SELECT * FROM secrets");
    }

Performance Best Practices

  • Keep server components at the top of the tree for maximum data fetching.
  • Move client components down as low as possible in the component tree.
  • Cache expensive server queries using fetch caching or unstable_cache.
    const data = await fetch('https://...', { next: { revalidate: 3600 } });
  • Use streaming with Suspense to show loading states while data fetches.
    <Suspense fallback={<p>Loading...</p>}>
      <ChildComponent />
    </Suspense>
  • Avoid passing large objects or functions to client components as props.

Tip: Keep the "use client" boundary as low as possible in the tree to maximize server rendering and minimize the client bundle size.

Warning: Never include environment secrets in client components, even as props, since they become visible in the browser bundle and HTML.

Next.js MiddlewareNext.js Testing