Server Components & Next.js
So far your React code has run in the browser: the server sends an almost empty HTML page plus a JavaScript bundle, and React builds the interface on the user's device. That works, but the user stares at a blank page until the JavaScript downloads, search engines see little content, and every library you use adds to the download.
Modern React can also run on the server. In this lesson you will compare client rendering, server rendering and React Server Components, learn the "use client" and "use server" directives, and get an overview of Next.js, the most widely used React framework. This is an overview to help you choose and read framework code; each framework has detailed documentation of its own.
Three ways to render
| Approach | What happens | Good for |
|---|---|---|
| Client-side rendering (CSR) | The browser downloads JavaScript and React renders everything there. A Vite app works this way. | Dashboards and tools behind a login |
| Server-side rendering (SSR) | The server runs React to produce full HTML for each request; the browser shows it at once, then React hydrates it (attaches event handlers) to make it interactive. | Public pages that must load fast and be indexed |
| Static generation (SSG) | The same HTML is produced once at build time and served from a CDN. | Blogs, documentation, marketing pages |
Hydration is the step where React, running in the browser, takes over HTML that the server already produced. The page is visible before hydration finishes, but buttons only work after it.
React Server Components
SSR still sends the JavaScript of every component to the browser for hydration. React Server Components (RSC) go further: some components run only on the server. Their code never reaches the browser; only their rendered result does. A Server Component:
- can be an
asyncfunction andawaitdata directly, from a database, the file system or a private API; - can use secrets such as database passwords safely, because its code stays on the server;
- adds zero JavaScript to the browser bundle, even if it uses a large library to format dates or Markdown;
- cannot use state, effects or event handlers, because nothing of it runs in the browser.
Components that need interactivity are Client Components. You mark the boundary by putting the "use client" directive at the top of a file. That file, and everything it imports, becomes part of the browser bundle:
// app/products/page.jsx: a Server Component (the default in Next.js)
import { db } from "@/lib/db";
import AddToCartButton from "./AddToCartButton";
export default async function ProductsPage() {
const products = await db.product.findMany({ where: { city: "Pune" } });
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}: ₹{p.price} <AddToCartButton productId={p.id} />
</li>
))}
</ul>
);
}// app/products/AddToCartButton.jsx: a Client Component
"use client";
import { useState } from "react";
export default function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)} disabled={added}>
{added ? "Added" : "Add to cart"}
</button>
);
}The product list is rendered on the server, with no fetch code, loading state or effect in the browser. Only the small button is shipped as JavaScript. Think of the page as server-rendered content with interactive "islands".
The rules for mixing them:
- Server Components can render Client Components and pass them props. Those props must be serialisable (strings, numbers, plain objects, arrays, dates, JSX), not functions.
- Client Components cannot import Server Components, but they can receive them as
childrenor other JSX props. - Mark only the files that need it with
"use client", as low in the tree as you can, to keep the bundle small.
Server Functions: "use server"
To change data from the browser, such as saving a form, a framework lets you write a Server Function: an async function marked with "use server". The browser can call it like a normal function, and the framework sends the request for you. Combined with form actions, a full-stack form needs no API route:
// app/feedback/actions.js
"use server";
import { db } from "@/lib/db";
export async function saveFeedback(formData) {
const message = String(formData.get("message")).slice(0, 500);
await db.feedback.create({ data: { message } });
}
// app/feedback/page.jsx (Server Component)
import { saveFeedback } from "./actions";
export default function FeedbackPage() {
return (
<form action={saveFeedback}>
<textarea name="message" required />
<button>Send</button>
</form>
);
}A Server Function is a public endpoint: anyone can call it. Always check the user's login and validate the input inside it, exactly as you would in any server API.
Streaming with Suspense
Server rendering works together with Suspense. The server can send the page shell immediately and stream the slow parts later, each one replacing its fallback when its data is ready. This runnable simulation shows the user's view of a streamed page, using promises that resolve at different times:
import { use, Suspense } from "react";
import { createRoot } from "react-dom/client";
const delay = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
// In a real framework these would be async Server Components
const profilePromise = delay(400, "Ananya Iyer, Kochi");
const ordersPromise = delay(1200, ["Idli steamer", "Filter coffee set"]);
const offersPromise = delay(2000, "10% off on kitchenware this week");
function Profile() {
return <p>Signed in as {use(profilePromise)}</p>;
}
function Orders() {
return <ul>{use(ordersPromise).map((o) => <li key={o}>{o}</li>)}</ul>;
}
function Offers() {
return <p>Offer: {use(offersPromise)}</p>;
}
function Page() {
return (
<main>
<h2>My account</h2>
<Suspense fallback={<p>Loading profile...</p>}><Profile /></Suspense>
<h3>Recent orders</h3>
<Suspense fallback={<p>Loading orders...</p>}><Orders /></Suspense>
<Suspense fallback={<p>Loading offers...</p>}><Offers /></Suspense>
</main>
);
}
createRoot(document.getElementById("root")).render(<Page />);The heading appears at once, then each section fills in as its data arrives. With real Server Components, the HTML for each section arrives from the server in the same order, without the browser running any of this data code.
Next.js in brief
You cannot use Server Components with a plain Vite setup; they need a framework that runs code on the server and bundles it correctly. The React team recommends starting new full-stack apps with a framework such as Next.js (App Router), React Router in framework mode, or Expo for mobile apps. Create a Next.js app with:
npx create-next-app@latest college-portalNext.js uses file-based routing in the app folder:
| File | Meaning |
|---|---|
app/layout.jsx | Root layout with <html> and <body>, shared by every page |
app/page.jsx | The page at / |
app/courses/page.jsx | The page at /courses |
app/courses/[slug]/page.jsx | A dynamic page such as /courses/bca; read the segment from params |
app/courses/loading.jsx | A Suspense fallback shown while the segment loads |
app/courses/error.jsx | An error boundary for the segment (must be a Client Component) |
Components in app are Server Components unless their file starts with "use client". In current Next.js versions params is a promise, so a dynamic page reads it with const { slug } = await params;.
Common mistakes
- Using hooks in a Server Component.
useState,useEffectandonClickneed"use client". - Marking everything
"use client". It works, but you lose the benefits; keep client files small and low in the tree. - Passing functions from server to client. Props crossing the boundary must be serialisable; pass data, or use a Server Function.
- Trusting Server Functions blindly. They are reachable from the internet; check authentication and validate input.
- Putting secrets in Client Components. Anything in a
"use client"file ends up in the browser bundle.
Key points
- CSR renders in the browser, SSR renders HTML per request and hydrates it, SSG renders at build time.
- Server Components run only on the server, can be
async, can read data directly and ship no JavaScript. "use client"marks a file as a Client Component, which can use state, effects and events."use server"marks Server Functions that the browser can call, for example as form actions.- Suspense lets the server stream slow parts of a page after the shell.
- Server Components need a framework such as Next.js; Next.js uses file-based routing in
app/.
Exercise
You are planning a Next.js page /results/[roll] for a school. It must read a student's marks from a database, show them in a table, and have a "Request re-check" button that changes to "Requested" when clicked. Decide which parts are Server Components and which are Client Components, and write both files in outline.
Show solution
// app/results/[roll]/page.jsx: Server Component
import { db } from "@/lib/db";
import RecheckButton from "./RecheckButton";
export default async function ResultPage({ params }) {
const { roll } = await params;
const result = await db.result.findUnique({ where: { roll } });
if (!result) return <p>No result for roll number {roll}.</p>;
return (
<>
<h1>Result for {result.name}</h1>
<table>
<tbody>
{result.subjects.map((s) => (
<tr key={s.name}><td>{s.name}</td><td>{s.marks}</td></tr>
))}
</tbody>
</table>
<RecheckButton roll={roll} />
</>
);
}
// app/results/[roll]/RecheckButton.jsx: Client Component
"use client";
import { useState } from "react";
export default function RecheckButton({ roll }) {
const [requested, setRequested] = useState(false);
return (
<button disabled={requested} onClick={() => setRequested(true)}>
{requested ? "Requested" : `Request re-check for ${roll}`}
</button>
);
}Reading the database and building the table happen on the server, so no data-fetching code reaches the browser. Only the button, which needs state and a click handler, is a Client Component. In a real app the click would also call a Server Function to save the request.
Quick Quiz
1. What is special about a React Server Component?
Server Components render on the server; only their output is sent to the browser.
2. Which directive marks a file as a Client Component?
Files starting with "use client" (and their imports) are bundled for the browser.
3. What is hydration?
Hydration makes server-rendered HTML interactive.
4. Why must you validate input inside a "use server" function?
Server Functions are public endpoints, so they need the same checks as any server API.
Learn React with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
