React Server Components Explained

React Server Components render on the server and send finished UI, not their JavaScript, to the browser. They can await data and use secrets directly. Anything interactive goes in a Client Component, a file starting with "use client". Forms can call server functions marked "use server", and Suspense streams slow parts in later.

The three directives and hooks you need
WriteMeans
(nothing)Server Component, in the Next.js App Router
"use client"This file and its imports run in the browser too
"use server"These exported functions run on the server when called
<Suspense>Show a fallback here while this part loads

Updated

Start with the diagram: blue boxes stay on the server, orange ones also ship to the browser. Then copy the patterns for data, forms and streaming, and use the error table when the build complains.

New to async code? Start with promises, async/await and fetch.

Boundary diagram

What runs where

Where each thing can run
You need toServer ComponentClient Component
Read a database or a secret API keyYesNo (would expose the secret)
await data directly in the componentYes (async component)No
useState, useEffect, useReducerNoYes
onClick, onChange and other handlersNoYes
window, localStorage, browser APIsNoYes, after mount
Ship zero JavaScript for the componentYesNo
Render the other kindYes, render Client ComponentsOnly Server Components passed in as children or props

Server and Client Components

A Server Component that reads data

In the App Router, components are Server Components unless marked otherwise. They can be async and talk to the database directly; none of this code reaches the browser.

// app/products/page.tsx (Server Component by default)
import { db } from "@/lib/db";
import { AddToCart } from "./add-to-cart";

export default async function ProductsPage() {
  const products = await db.product.findMany({ take: 20 });
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>
          {p.name} ({p.price})
          <AddToCart productId={p.id} />
        </li>
      ))}
    </ul>
  );
}

A Client Component for interaction

"use client" at the very top marks a boundary: this file and everything it imports is bundled for the browser. Keep these files small and push them to the leaves of the tree.

// app/products/add-to-cart.tsx
"use client";
import { useState } from "react";

export function AddToCart({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => setAdded(true)} disabled={added}>
      {added ? "Added" : "Add to cart"}
    </button>
  );
}

Passing a Server Component through a Client Component

A Client Component cannot import a Server Component, but it can receive one as children. The server renders it first and the client only places the result.

// app/components/drawer.tsx
"use client";
import { useState, type ReactNode } from "react";

export function Drawer({ children }: { children: ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button onClick={() => setOpen(!open)}>Filters</button>
      {open && <aside>{children}</aside>}
    </>
  );
}

// app/shop/page.tsx (server)
<Drawer>
  <CategoryList />   {/* Server Component: queries the database */}
</Drawer>

Keep secrets out of the browser by accident

Importing the server-only package makes the build fail if a client file ever imports this module, so an API key cannot leak through a careless import.

// lib/payments.ts
import "server-only";

export async function getBalance(accountId: string) {
  const res = await fetch(`https://api.example-payments.com/balance/${accountId}`, {
    headers: { Authorization: `Bearer ${process.env.PAYMENTS_SECRET_KEY}` },
  });
  return res.json();
}

Server functions for forms

A form that posts to a server function

"use server" turns an async function into an endpoint the form can call. The form works before JavaScript loads, because it is a real form post.

// app/newsletter/actions.ts
"use server";
import { z } from "zod";

const Email = z.string().email();

export async function subscribe(prev: { message: string }, formData: FormData) {
  const parsed = Email.safeParse(formData.get("email"));
  if (!parsed.success) return { message: "Please enter a valid email." };
  await saveSubscriber(parsed.data);
  return { message: "Thanks, you are on the list." };
}

Pending and result state with useActionState

useActionState (React 19) wraps the server function and gives you its last result and a pending flag, so the button can say Sending and the message can appear without extra state.

// app/newsletter/form.tsx
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";

export function NewsletterForm() {
  const [state, action, pending] = useActionState(subscribe, { message: "" });
  return (
    <form action={action}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      <button disabled={pending}>{pending ? "Sending..." : "Subscribe"}</button>
      <p aria-live="polite">{state.message}</p>
    </form>
  );
}

Refresh the page data after a change

After a mutation, tell Next.js which path's data is stale so the next render shows the change.

"use server";
import { revalidatePath } from "next/cache";

export async function deleteProduct(id: string) {
  await db.product.delete({ where: { id } });
  revalidatePath("/products");
}

Streaming with Suspense

Stream the slow part with Suspense

Wrap a slow Server Component in Suspense. The rest of the page is sent at once with the fallback in place, and the slow part streams in when its data arrives.

import { Suspense } from "react";

// In current Next.js versions params is a promise.
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <>
      <ProductDetails id={id} />        {/* fast */}
      <Suspense fallback={<p>Loading reviews...</p>}>
        <Reviews id={id} />             {/* slow query */}
      </Suspense>
    </>
  );
}

loading.tsx for a whole route

A loading.tsx file next to page.tsx wraps that page in a Suspense boundary for you, so navigation shows it straight away while the page's data loads.

// app/products/loading.tsx
export default function Loading() {
  return <div className="skeleton-grid" aria-busy="true">Loading products...</div>;
}

Start requests together

Awaiting one fetch after another in a Server Component adds their times together. Start them at once and await both.

export default async function Dashboard() {
  const [orders, visits] = await Promise.all([getOrders(), getVisits()]);
  return <Stats orders={orders} visits={visits} />;
}

Errors you will meet

Common errors and the fix
What you seeCauseFix
A hook such as useState only works in a Client ComponentA hook in a file without "use client"Move the interactive part into its own file that starts with "use client"
Event handlers cannot be passed to Client Component propsonClick passed from a Server ComponentPut the handler inside the Client Component, or pass a server function instead
Functions cannot be passed directly to Client ComponentsA plain function passed as a prop across the boundaryMark it "use server" if it should run on the server, or define it in the client file
Only plain objects can be passed to Client ComponentsClass instances such as Date-heavy models or Maps sent as propsPass plain data (strings, numbers, arrays, plain objects)
window is not definedBrowser API used while rendering on the serverRead it inside useEffect, or check typeof window first
Hydration mismatchServer and browser rendered different text (dates, random values)Render the changing part only on the client, or pass the value from the server

Why this model helps a website

Most of a marketing or content page never changes after it renders: headings, text, images, product cards. As Server Components they cost no JavaScript at all, which leaves the main thread free for the few interactive parts and usually improves Interaction to Next Paint.

Security checklist for server functions

Treat each "use server" function like an API route: check the session, validate the input with a schema, check the user may change that record, and return only what the client needs. Never return database rows with fields the user should not see.

How it works

  1. Leave components as Server Components by default.
  2. Fetch data in async Server Components, close to where it is shown.
  3. Add "use client" only to the small parts that need state or handlers.
  4. Use server functions for forms and mutations, with useActionState for feedback.
  5. Wrap slow parts in Suspense, or add a loading.tsx, so the page shows up fast.

Tips

Frequently asked questions

Are Server Components the same as server-side rendering?

No. Server-side rendering turns any component into HTML for the first load and still ships its JavaScript. A Server Component never ships its JavaScript; only its output does. Client Components are still server-rendered for the first load.

Do I need Next.js to use Server Components?

They need a framework or bundler that supports them. The Next.js App Router is the most widely used; a few others, such as Waku, support them as well.

Can a Client Component import a Server Component?

No, an import from a client file pulls the module into the client bundle. Instead, pass the Server Component as children or another prop from a Server Component parent.

What is the difference between server actions and server functions?

React now calls any function marked "use server" a server function. When one is used as a form action or called inside a transition, it is often called a server action. Next.js used the term server actions first.

Does useActionState work without JavaScript?

The form still posts to the server function before hydration, so it submits. Pending state and the inline message need JavaScript, so make sure the server returns a useful page either way.

Related: React Hook Form examples, React memo, useMemo and useCallback, Promises, async/await and fetch, React Router examples, All free tools.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap