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 useEffect data 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 .