React Server Components Explained for MERN Developers
Translate your MERN mental model to Server Components: fetch data directly in components, add small client components for interactivity, and save data with secure Server Functions.

Mohit Malaya Nandy
Software Engineer & Product Builder

On this page
If you learned full-stack development with the MERN stack, your mental model is clear: React runs in the browser, Express serves a JSON API, and the two talk over fetch. React Server Components change that model, and at first they can feel confusing.
This guide translates Server Components into MERN terms. You will see the same feature, a product list with an "add to wishlist" button, built the MERN way and the Server Components way, and learn exactly when to use each kind of component. Examples use Next.js, the most popular framework for Server Components, and follow the React and Next.js documentation.
The mental model shift#
| Question | Classic MERN | With Server Components |
|---|---|---|
| Where does React run? | In the browser | On the server and in the browser |
| How does a page get data? | useEffect + fetch('/api/…') | await the database directly in the component |
| Who renders the HTML? | The browser, after JavaScript loads | The server; the browser receives the result |
| How do forms save data? | fetch POST to an Express route | A Server Function (Server Action) |
| What code reaches the browser? | All components | Only client components |
The core idea: a component can now run on the server. It can read from MongoDB, use secret environment variables and import large libraries, and none of that code or data is sent to the browser, only the rendered result.
The MERN way: a product list#
Here is the familiar version. First, an Express route:
router.get('/api/products', async (req, res) => {
const products = await Product.find().limit(20).lean();
res.json(products);
});Then a React component that fetches it after the page loads:
export default function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then((res) => res.json())
.then((data) => setProducts(data))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Loading…</p>;
return <ul>{products.map((p) => <li key={p._id}>{p.name}</li>)}</ul>;
}It works, but the browser must download React and your code, run it, call the API, wait for the database, then render. Users see a spinner, and search engines may see an empty list.
The Server Components way#
In Next.js, components in the app folder are Server Components by default. They can be async and read data directly (fetching data):
import { getProducts } from '@/lib/products';
import WishlistButton from './WishlistButton';
export default async function ProductsPage() {
const products = await getProducts(); // runs on the server
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} - ₹{p.price}
<WishlistButton productId={p.id} />
</li>
))}
</ul>
);
}import 'server-only'; // build error if a client component imports this file
import { connectDB } from './db';
import Product from './models/Product';
export async function getProducts() {
await connectDB();
const docs = await Product.find().limit(20).lean();
// Return plain, serialisable data with only the fields the page needs
return docs.map((d) => ({ id: d._id.toString(), name: d.name, price: d.price }));
}- No API route, no useEffect, no loading state for the initial data: the HTML arrives with the products already in it.
import 'server-only'protects database code and secrets: if anyone imports this file into a client component, the build fails (docs).- Mapping to plain objects matters: only serialisable data (strings, numbers, plain objects, arrays) can be passed from server to client components. A Mongoose document or
ObjectIdcannot.
Adding interactivity: client components#
Server Components cannot use state or event handlers. When you need them, create a client component by adding 'use client' at the top:
'use client';
import { useTransition, useState } from 'react';
import { addToWishlist } from './actions';
export default function WishlistButton({ productId }) {
const [saved, setSaved] = useState(false);
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending || saved}
onClick={() =>
startTransition(async () => {
const result = await addToWishlist(productId);
if (result.ok) setSaved(true);
})
}
>
{saved ? 'Saved' : pending ? 'Saving…' : 'Add to wishlist'}
</button>
);
}'use client' marks a boundary: this file and everything it imports is sent to the browser. The rest of the page stays on the server. Keep client components small and as far down the tree as possible, like this button, rather than marking a whole page as a client component.
Saving data: Server Functions (Server Actions)#
Instead of an Express POST route, you can write a function marked with 'use server' and call it from the client. React turns the call into an HTTP request for you (mutating data):
'use server';
import * as z from 'zod';
import { getCurrentUser } from '@/lib/auth';
import { connectDB } from '@/lib/db';
import Wishlist from '@/lib/models/Wishlist';
const ProductId = z.string().regex(/^[a-f0-9]{24}$/);
export async function addToWishlist(productId) {
// 1. Authenticate: this function is a public endpoint
const user = await getCurrentUser();
if (!user) return { ok: false, error: 'Please sign in' };
// 2. Validate input: never trust what the browser sends
const parsed = ProductId.safeParse(productId);
if (!parsed.success) return { ok: false, error: 'Invalid product' };
// 3. Do the work, scoped to the current user
await connectDB();
await Wishlist.updateOne(
{ userId: user.id },
{ $addToSet: { productIds: parsed.data } },
{ upsert: true }
);
return { ok: true };
}Server or client? A quick guide#
| You need to… | Use |
|---|---|
| Read from a database or API | Server Component |
| Use secrets (API keys, tokens) | Server Component |
| Render mostly static content | Server Component |
| Use useState, useEffect or useReducer | Client component |
| Handle onClick, onChange and other events | Client component |
| Use browser APIs (localStorage, window, geolocation) | Client component |
| Save or change data | Server Function, called from a form or client component |
Do you still need Express?#
Sometimes, yes. Keep a separate API when:
- A mobile app, another website or a third party needs the same data.
- You have long-running jobs, WebSockets or work that does not belong to a page.
- Different teams own the frontend and backend and deploy them separately.
Even then, Server Components help: they can call your Express API from the server, keeping tokens and internal URLs out of the browser. For a website that is its own only client, you can often skip the separate API entirely.
Common mistakes#
- Putting 'use client' at the top of every page. You lose the benefits. Push it down to the interactive leaves.
- Passing non-serialisable props (Mongoose documents, class instances, functions) from a server to a client component.
- Importing server code into a client component, which can leak secrets into the browser bundle. Use
server-onlyto catch it. - Skipping auth checks in Server Functions because they "feel internal".
- Fetching in a client useEffect by habit when a Server Component could load the data before the page is sent.
Frequently asked questions#
What are React Server Components?
React Server Components are components that run only on the server. They can read from a database or file system directly, and their code is never sent to the browser. Only their rendered result is.
Do I still need Express if I use Server Components?
For many apps, not for your own website's data: Server Components can read the database directly and Server Functions handle form submissions. Keep a separate API when mobile apps, other services or third parties also need it.
When should I use 'use client'?
Add 'use client' to components that need interactivity or browser features: useState, useEffect, event handlers like onClick, or APIs such as localStorage. Keep these components as small and as low in the tree as you can.
Are Server Actions secure?
Server Actions (Server Functions) are public HTTP endpoints, even though they look like normal functions. Always check authentication and authorization inside every action, and validate its input, just as you would in an Express route.
Can a Server Component use useState or useEffect?
No. Server Components render once on the server and have no state or lifecycle in the browser. Move the interactive part into a small client component and render it from the Server Component.
Official references#
- Server Components, 'use client', 'use server' and Server Functions, React
- Server and Client Components, fetching data and mutating data, Next.js
- Data security, Next.js
Found this useful? You can support my writing on GitHub Sponsors or with a coffee.


