Next.js Directives

Use use server and use client directives to control rendering and data access.

TL;DR

  1. 01"use client" marks components to render on the client.
  2. 02"use server" marks functions to run only on the server.
  3. 03Server components are the default in the App Router.

Tips

  1. 01Use server components for fetching data and accessing secrets — it's faster and more secure than client components.

Warnings

  1. 01"use client" at the top of a file makes the entire file and its imports client-side — keep client-heavy components in separate files.

Default Server Rendering

  • Every component in the App Router is a Server Component by default — no directive needed.
    // app/page.tsx — server component, no directive required
    export default async function Home() {
      const res = await fetch("https://api.example.com/data", { cache: "no-store" });
      const data = await res.json();
      return <div>{data.title}</div>;
    }
  • Server Components run only on the server — no JavaScript is shipped to the browser for them.
  • They can use async/await, read environment variables, and query databases securely.
    export default async function AdminPage() {
      const users = await db.user.findMany();
      return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
    }
  • Server Components cannot use React hooks or event handlers — add a directive to opt into client rendering only when needed.
    // ❌ Not allowed in a Server Component — move to a Client Component
    // const [count, setCount] = useState(0);
    // window.addEventListener(...);
  • Use React cache() to deduplicate repeated server-side data calls across components.
    import { cache } from "react";
    
    export const getUser = cache(async (id: string) => db.user.findUnique({ where: { id } }));
    // getUser(id) called in Header and Sidebar = only one DB query

Client Directive Scope

  • "use client" must be the very first line of the file — before any imports.
    "use client";
    // ↑ must appear before any import statements
    import { useState } from "react";
    
    export default function Counter() {
      const [count, setCount] = useState(0);
      return <button onClick={() => setCount(count + 1)}>{count}</button>;
    }
  • The directive marks the entire file AND all modules it imports as client-side — contamination spreads through imports.
    "use client";
    // Both files below become part of the client bundle:
    import { Chart } from "./Chart";       // ← client-side
    import { DataGrid } from "./DataGrid";   // ← client-side
  • Avoid importing heavy utilities into a "use client" file — they inflate the browser bundle.
    "use client";
    // ❌ Pulls entire lodash into the client bundle
    import _ from "lodash";
    
    // ✅ Move heavy computation to a Server Component or Server Action
  • Split a large client component into smaller files to minimise the client boundary.
    // Good: only the interactive button is a Client Component
    // app/post/page.tsx — Server Component (no directive)
    import { LikeButton } from "./LikeButton"; // only this file has "use client"
    export default async function PostPage() {
      const post = await getPost();
      return <article>{post.body}<LikeButton id={post.id} /></article>;
    }
  • Third-party components that use hooks or browser APIs must be wrapped with "use client".
    // components/MapWrapper.tsx
    "use client";
    import { MapComponent } from "some-map-library"; // uses window internally
    export default function MapWrapper({ center }) {
      return <MapComponent center={center} />;
    }

Server Functions

  • Mark functions to run only on the server with "use server".
    "use server";
    
    export async function submitForm(formData: FormData) {
      const email = formData.get("email");
      // Save to database securely
      return { success: true };
    }
  • Server functions can be called from client components.
    "use client";
    
    async function handleSubmit(formData: FormData) {
      const result = await submitForm(formData);
    }
  • Functions run securely on the server.
  • Use server functions with HTML form actions for progressive enhancement.
    // app/form.tsx (server component)
    import { saveContact } from "./actions";
    
    export default function ContactForm() {
      return <form action={saveContact}><input name="email" /><button>Send</button></form>;
    }
  • Validate input inside server functions before writing to the database.
    "use server";
    
    export async function createPost(formData: FormData) {
      const title = formData.get("title") as string;
      if (!title || title.length < 3) return { error: "Title too short" };
      await db.post.create({ data: { title } });
      return { ok: true };
    }

Directive Boundary Patterns

  • A Client Component can receive a Server Component as a child via props — the child still runs on the server.
    "use client";
    // ClientShell.tsx — the children prop can be a Server Component
    export default function ClientShell({ children }) {
      return <div>{children}</div>;
    }
    
    // app/page.tsx (Server Component) — ServerSidebar stays on the server
    <ClientShell><ServerSidebar /></ClientShell>
  • A Client Component cannot directly import a Server Component — this would pull server code into the client bundle.
    "use client";
    // ❌ Cannot import a server component directly from a client file
    import { ServerSidebar } from "./ServerSidebar"; // runtime error
  • Functions cannot cross the server-client boundary as props — use Server Actions instead.
    // ❌ Plain server function cannot be passed as a prop
    <ClientComp onClick={serverFunction} />
    
    // ✅ Server Action CAN be passed as a prop
    "use server";
    export async function deleteItem(id: string) { await db.item.delete({ where: { id } }); }
    // Then in a Server Component: <ClientComp action={deleteItem} />
  • Only JSON-serialisable values can be passed as props across the server-client boundary.
    // ✅ OK: strings, numbers, arrays, plain objects, Promises
    <ClientChart data={data} title="Sales" />
    
    // ❌ Not OK: class instances, Maps, Sets, functions (unless Server Actions)
    <ClientComp handler={someClassInstance} />
  • Verify the boundary by checking the Network tab — server component code should not appear in JS bundles.
    # Open DevTools > Network > JS
    # Server-only modules (db, secrets) should NOT appear in client chunks

Directive Design Patterns

  • Use server components by default for performance.
    // Good: server component fetches data
    export default async function Posts() {
      const posts = await getPosts();
      return posts.map(post => <Post key={post.id} post={post} />);
    }
  • Mark leaf components as "use client" for interactivity.
    "use client";
    // Only interactive components
    export function Favorite({ postId }) {
      const [liked, setLiked] = useState(false);
      return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>;
    }
  • Use server-only package to prevent accidental client imports.
    import "server-only";
    // This file throws an error if imported in a client bundle
    export async function getSecretData() { ... }
  • Avoid passing non-serializable values like functions as props.
    // Wrong: functions can't cross the server-client boundary as props
    <ClientComp onClick={serverFunction} /> // ❌
    // Use server actions instead
    <ClientComp action={serverAction} />    // ✅
  • Test directive placement by checking the Network tab for unexpected JS.
    # Open browser DevTools > Network > JS
    # Check that server-only components don't appear in client bundles

FAQ