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.
| Write | Means |
|---|---|
| (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.
| You need to | Server Component | Client Component |
|---|---|---|
| Read a database or a secret API key | Yes | No (would expose the secret) |
| await data directly in the component | Yes (async component) | No |
| useState, useEffect, useReducer | No | Yes |
| onClick, onChange and other handlers | No | Yes |
| window, localStorage, browser APIs | No | Yes, after mount |
| Ship zero JavaScript for the component | Yes | No |
| Render the other kind | Yes, render Client Components | Only Server Components passed in as children or props |
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>
);
}"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>
);
}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>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();
}"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." };
}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>
);
}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");
}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>
</>
);
}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>;
}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} />;
}| What you see | Cause | Fix |
|---|---|---|
| A hook such as useState only works in a Client Component | A 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 props | onClick passed from a Server Component | Put the handler inside the Client Component, or pass a server function instead |
| Functions cannot be passed directly to Client Components | A plain function passed as a prop across the boundary | Mark 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 Components | Class instances such as Date-heavy models or Maps sent as props | Pass plain data (strings, numbers, arrays, plain objects) |
| window is not defined | Browser API used while rendering on the server | Read it inside useEffect, or check typeof window first |
| Hydration mismatch | Server and browser rendered different text (dates, random values) | Render the changing part only on the client, or pass the value from the server |
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.
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.
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.
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.
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.
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.
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.
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.