React Server Components (RSC) cheatsheet.
Server vs client
In an RSC framework (Next.js, Remix…):
- Server components: run on server. Can be async. Read DB/files directly. Don’t ship to client.
- Client components: run in browser. Have state, effects, event handlers. Marked with
"use client".
Default is server
// app/page.tsx — server component
async function Page() {
const users = await db.user.findMany();
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
No "use client" needed.
“use client” boundary
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Everything imported into a "use client" file becomes part of the client bundle.
Server → Client
// server component
import { Counter } from "./Counter"; // client component
export default async function Page() {
const initial = await db.count();
return <Counter initial={initial} />;
}
Props must be serializable: numbers, strings, plain objects, dates. NOT functions, classes, Promises, JSX returned from server.
Client → Server (slot pattern)
You can pass server children to a client component:
// app/page.tsx (server)
import { ClientShell } from "./ClientShell";
export default function Page() {
return (
<ClientShell>
<ServerWidget /> // still server-rendered, slotted in
</ClientShell>
);
}
// ClientShell (client) just renders { children }
Async server components
async function User({ id }: { id: number }) {
const user = await db.user.find(id);
return <h1>{user.name}</h1>;
}
No useEffect for data — just await.
Suspense with server components
<Suspense fallback={<Loader />}>
<SlowAsyncServerComp />
</Suspense>
Server streams the fallback first, replaces with content when ready.
Server actions
"use server";
export async function createPost(formData: FormData) {
await db.post.create({ data: { title: formData.get("title") as string } });
revalidatePath("/posts");
}
Call from form action OR client component:
// client form
"use client";
import { createPost } from "./actions";
<form action={createPost}>
<input name="title" />
<button>Post</button>
</form>
Data fetching pattern
// page.tsx
async function Page({ params }) {
// Run in parallel
const [user, posts] = await Promise.all([
fetchUser(params.id),
fetchPosts(params.id),
]);
return (
<>
<User u={user} />
<Posts ps={posts} />
</>
);
}
fetch caching (Next.js)
fetch(url, { cache: "force-cache" }) // SSG-like, default in builds
fetch(url, { cache: "no-store" }) // always fresh
fetch(url, { next: { revalidate: 60 } }) // ISR
fetch(url, { next: { tags: ["users"] } }) // tag-based revalidate
revalidate
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath("/posts");
revalidateTag("users");
Cookies / headers in server components
import { cookies, headers } from "next/headers";
async function Page() {
const c = cookies();
const session = c.get("session")?.value;
...
}
What NOT to do in server components
- No
useState,useEffect,useContext,useRef. - No DOM access (no
document,window). - No browser-only libraries.
- No event handlers (
onClick, etc). - No setState callbacks.
If you need any of these → make it a client component.
Where to put state
Push client components to the leaves. Most of the tree should be server.
ServerLayout
ServerHeader
ClientUserMenu <- only this is client
ServerPage
ServerPostList
ServerPostItem
ClientLikeButton <- only this
Server-only utilities
import "server-only"; // throws if imported from client
export async function dbQuery() { ... }
import "client-only"; // throws if imported on server
Protects against accidental bundling.
Streaming pattern
import { Suspense } from "react";
export default function Page() {
return (
<>
<Header /> {/* fast */}
<Suspense fallback={<UserSkel />}>
<User /> {/* await */}
</Suspense>
<Suspense fallback={<FeedSkel />}>
<Feed /> {/* await */}
</Suspense>
</>
);
}
Header flushes immediately, User and Feed stream in.
Common mistakes
- Forgetting
"use client"in interactive component → “useState only in client components.” - Passing function/JSX from server → client component as prop.
- Calling DB directly from client component.
- Mixing
useEffectdata fetching with server fetching — pick one. - Putting
"use client"at root → entire app is client.
Read this next
If you want my RSC + server actions patterns, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .