React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

server-componentsrscasync-componentszero-jsserver-rendering

Server Components

Render React components on the server with zero client-side JavaScript -- the default in Next.js App Router.

Recipe

Quick-reference recipe card -- copy-paste ready.

// app/page.tsx -- Server Component by default (no directive needed)
import { db } from "@/lib/db";
 
export default async function HomePage() {
  const posts = await db.post.findMany({ take: 10 });
 
  return (
    <main>
      <h1>Latest Posts</h1>
      <ul>
        {posts.map((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </main>
  );
}

When to reach for this: Any component that only reads data and renders markup -- no useState, no useEffect, no event handlers, no browser APIs. This is the default; you opt out with "use client", not in.

Working Example

// app/blog/page.tsx (Server Component)
import { Suspense } from "react";
import { formatDistanceToNow } from "date-fns";
 
type Post = {
  id: string;
  title: string;
  excerpt: string;
  publishedAt: string;
  author: { name: string; avatar: string };
};
 
async function fetchPosts(): Promise<Post[]> {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 300 },
  });
  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}
 
export default async function BlogPage() {
  return (
    <main className="max-w-3xl mx-auto p-6">
      <h1 className="text-3xl font-bold mb-8">Blog</h1>
      <Suspense fallback={<PostsSkeleton />}>
        <PostList />
      </Suspense>
    </main>
  );
}
 
async function PostList() {
  const posts = await fetchPosts();
 
  return (
    <div className="space-y-8">
      {posts.map((post) => (
        <article key={post.id} className="border-b pb-6">
          <h2 className="text-xl font-semibold mb-2">{post.title}</h2>
          <p className="text-gray-600 mb-3">{post.excerpt}</p>
          <div className="flex items-center gap-3 text-sm text-gray-500">
            <img
              src={post.author.avatar}
              alt={post.author.name}
              className="w-6 h-6 rounded-full"
            />
            <span>{post.author.name}</span>
            <span>
              {formatDistanceToNow(new Date(post.publishedAt), {
                addSuffix: true,
              })}
            </span>
          </div>
        </article>
      ))}
    </div>
  );
}
 
function PostsSkeleton() {
  return (
    <div className="space-y-8">
      {Array.from({ length: 3 }).map((_, i) => (
        <div key={i} className="border-b pb-6">
          <div className="h-6 bg-gray-200 rounded w-3/4 mb-2 animate-pulse" />
          <div className="h-4 bg-gray-100 rounded w-full mb-3 animate-pulse" />
          <div className="h-4 bg-gray-100 rounded w-1/2 animate-pulse" />
        </div>
      ))}
    </div>
  );
}

What this demonstrates:

  • Async Server Component fetching data with await directly in the function body
  • Using a heavy library (date-fns) that ships zero JavaScript to the client
  • Separating the async data fetch into a child component wrapped in <Suspense> for streaming
  • No "use client" directive anywhere -- the entire page is server-rendered

Deep Dive

How It Works

  • Components are Server Components by default in the App Router. No directive is needed.
  • Server Components execute on the server and produce an RSC payload -- a compact serialized React tree that is streamed to the client.
  • Server Components can be async and use await directly. This is not allowed in Client Components.
  • They can access databases, file systems, environment variables, and any server-only resource directly.
  • Dependencies used only in Server Components (e.g., markdown parsers, syntax highlighters, date-fns) are not included in the client bundle.
  • Server Components do not re-render on the client in response to state changes. They only re-execute when the route changes or revalidatePath/revalidateTag is called.
  • The RSC payload is reconciled with the client-side DOM by React, preserving Client Component state during navigations.

Variations

Parallel data fetching:

async function Dashboard() {
  const [users, revenue, orders] = await Promise.all([
    fetchUsers(),
    fetchRevenue(),
    fetchOrders(),
  ]);
 
  return (
    <>
      <UserTable users={users} />
      <RevenueChart revenue={revenue} />
      <OrderList orders={orders} />
    </>
  );
}

Passing server data to Client Components:

// Server Component
import { ClientMap } from "./client-map";
 
export default async function LocationPage() {
  const locations = await db.location.findMany();
 
  // Only serializable data can cross the boundary
  return <ClientMap locations={locations} />;
}

Server-only utilities:

// lib/server-only-utils.ts
import "server-only"; // Throws a build error if imported in a Client Component
 
export function getSecretConfig() {
  return {
    apiKey: process.env.SECRET_API_KEY!,
    dbUrl: process.env.DATABASE_URL!,
  };
}

TypeScript Notes

// Async Server Components return Promise<JSX.Element>
// TypeScript handles this with React 19+ types
async function MyComponent(): Promise<JSX.Element> {
  const data = await fetchData();
  return <div>{data.name}</div>;
}
 
// Props must be serializable when passed to Client Components
type SerializableProps = {
  name: string;
  count: number;
  items: { id: string; label: string }[];
  // NOT allowed: onClick: () => void
  // NOT allowed: ref: React.Ref<HTMLDivElement>
};
 
// Use `server-only` package for compile-time protection
import "server-only";

Gotchas

  • Cannot use hooks -- useState, useEffect, useRef, and all other hooks are client-only. Fix: Extract interactive parts into a "use client" component.

  • Cannot use event handlers -- onClick, onChange, onSubmit, etc. require client-side JavaScript. Fix: Move event-handling logic to a Client Component.

  • Cannot access browser APIs -- window, document, localStorage, navigator are not available on the server. Fix: Use these only in "use client" components or behind typeof window !== "undefined" checks.

  • Props to Client Components must be serializable -- Functions (except Server Actions), class instances, Symbols, and DOM nodes cannot be passed as props across the server-client boundary. Fix: Pass only plain data; use Server Actions for callbacks.

  • Large server payloads -- Fetching too much data in a Server Component and passing it all as props bloats the RSC payload. Fix: Fetch only what the Client Component needs; paginate on the server.

  • Third-party libraries may not be RSC-compatible -- Libraries that import useState, useEffect, or browser APIs fail in Server Components. Fix: Import them only inside "use client" files, or use a wrapper component.

Alternatives

ApproachUse WhenDon't Use When
Server ComponentsRead-only UI, data fetching, zero-JS renderingInteractive UI with state or effects
Client ComponentsInteractive UI with hooks, events, browser APIsPure data display with no interactivity
Server-side rendering (SSR)Legacy pre-RSC apps that need server HTMLYou have access to the App Router
Static Site GenerationContent rarely changes and can be built at deploy timeData is user-specific or highly dynamic
API routes + client fetchExternal consumers need a REST endpointData is only consumed by your own pages

Real-World Example

From a production Next.js 15 / React 19 SaaS application (SystemsArchitect.io).

// Production example: FAQ category page with direct data access
// File: src/app/faqs/[slug]/page.tsx
export default async function FaqCategoryPage({ params }: FaqCategoryPageProps) {
  const { slug } = await params;
  const category = await getFaqCategory(slug);
 
  if (!category) {
    notFound();
  }
 
  const iconConfig = getFaqIcon(category.slug);
  const IconComponent = iconConfig.icon;
 
  return (
    <div className="min-h-screen bg-zinc-50 dark:bg-black">
      <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
        <Link href="/faqs" className="inline-flex items-center gap-2 text-sm cursor-pointer">
          <ChevronLeft className="h-4 w-4" />
          <span>Back to all FAQs</span>
        </Link>
        <div className="flex items-center gap-3 mb-4">
          <div className={iconConfig.color}>
            <IconComponent className="h-8 w-8" />
          </div>
          <h1 className="text-3xl font-bold">{category.title}</h1>
        </div>
        <FaqList faqs={category.faqs} categorySlug={category.slug} />
      </div>
    </div>
  );
}

What this demonstrates in production:

  • No "use client" directive, this is a Server Component by default
  • The function is async and directly awaits data from getFaqCategory() which calls Prisma under the hood
  • await params is the Next.js 15+ pattern where params is now a Promise in dynamic routes
  • notFound() from next/navigation triggers the nearest not-found.tsx boundary
  • FaqList (a Client Component) receives pre-fetched data as props. The server/client boundary is at the prop level
  • Zero client JavaScript for this page's data fetching logic

FAQs

What makes a component a Server Component in Next.js?

Components are Server Components by default in the App Router. No directive is needed. They become Client Components only when you add "use client" to the file.

Can Server Components be async and use await?

Yes. Server Components can be async functions and use await directly in the function body. This is not allowed in Client Components.

Do Server Components ship JavaScript to the client?

No. Server Components produce an RSC payload (serialized React tree) that is streamed to the client. Dependencies used only in Server Components (e.g., date-fns, markdown parsers) are not included in the client bundle.

Gotcha: I tried using useState in a Server Component and got an error. Why?

Server Components cannot use React hooks (useState, useEffect, useRef, etc.) because they run on the server and do not re-render on the client. Extract interactive parts into a "use client" component.

Gotcha: A third-party library fails when imported in a Server Component. What do I do?

The library likely imports useState, useEffect, or browser APIs internally. Import it only inside a "use client" file, or create a thin Client Component wrapper around it.

What data can I pass from a Server Component to a Client Component?

Only serializable data: strings, numbers, booleans, arrays, plain objects, and Server Actions. You cannot pass regular functions, class instances, Symbols, or DOM nodes.

How do I protect server-only code from being imported in a Client Component?

Use the server-only package:

import "server-only";
 
export function getSecretConfig() {
  return { apiKey: process.env.SECRET_API_KEY! };
}

This throws a build error if the file is imported in a "use client" file.

How do I fetch data in parallel in a Server Component?

Use Promise.all to run multiple fetches concurrently:

const [users, revenue, orders] = await Promise.all([
  fetchUsers(),
  fetchRevenue(),
  fetchOrders(),
]);
When do Server Components re-execute?

Server Components do not re-render in response to client-side state changes. They re-execute only when the route changes or when revalidatePath/revalidateTag is called.

What is the TypeScript return type of an async Server Component?
async function MyComponent(): Promise<JSX.Element> {
  const data = await fetchData();
  return <div>{data.name}</div>;
}

React 19+ types handle Promise<JSX.Element> for async components.

How do I type props that will be passed from a Server Component to a Client Component in TypeScript?

Define a type with only serializable fields:

type SerializableProps = {
  name: string;
  count: number;
  items: { id: string; label: string }[];
  // NOT allowed: onClick: () => void
};
What is the RSC payload and how does it work?
  • The RSC payload is a compact serialized React tree produced by Server Components on the server.
  • It is streamed to the client where React reconciles it with the client-side DOM.
  • Client Component state is preserved during navigations thanks to this reconciliation.