From nextjs-plugin
React Server Components vs Client Components in Next.js: the boundary discipline, "use client" / "use server" directives, Server Actions, what serializes across the boundary, common pitfalls. Use this skill to: - Decide whether a component should be RSC or Client. - Push the "use client" boundary as deep as possible. - Implement Server Actions correctly (auth, validation, revalidation). - Compose RSC and Client components without breaking the model. - Pass data across the boundary safely. Do NOT use this skill for: - General Next.js conventions (see nextjs-conventions). - Specific data-fetching APIs and caching (see nextjs-data-fetching). - Routing (see nextjs-routing).
How this skill is triggered — by the user, by Claude, or both
Slash command
/nextjs-plugin:server-component-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The single most important skill for working in App Router. Get the boundary wrong and you'll get build errors, runtime errors, or silent serialization failures.
The single most important skill for working in App Router. Get the boundary wrong and you'll get build errors, runtime errors, or silent serialization failures.
async/await, server-only modules (DB clients, fs, filesystem), env vars (including secrets). Does NOT have: useState, useEffect, useRef, browser APIs (window, document), event handlers."use server". Callable from Client Components but execute on the server. Form-friendly via <form action={action}>.Default = RSC. A file is RSC unless its first non-import line is "use client".
"use client"A file MUST be Client when it uses any of:
useState, useReducer, useEffect, useLayoutEffect, useRef, useContext, useMemo, useCallback, custom hooks that use the above.onClick, onChange, onSubmit, onKeyDown, etc.window, document, localStorage, navigator, IntersectionObserver, etc.A file MAY be Client when:
A file SHOULD be RSC when:
Anti-pattern:
// ❌ Whole page is Client just because the filter is interactive
'use client';
import { useState } from 'react';
import { db } from '@/lib/db'; // BREAKS — server-only
export default function UsersPage() {
const [filter, setFilter] = useState('');
const users = await db.users.findMany(); // BREAKS — async + server only
return <div>...</div>;
}
Correct:
// app/users/page.tsx — RSC
import { db } from '@/lib/db';
import { UserFilter } from './_components/UserFilter';
import { UserList } from './_components/UserList';
export default async function UsersPage() {
const users = await db.users.findMany();
return (
<div>
<UserFilter /> {/* Client — interactive */}
<UserList users={users} /> {/* RSC — just renders data */}
</div>
);
}
// app/users/_components/UserFilter.tsx — Client (only the filter)
'use client';
import { useState } from 'react';
export function UserFilter() {
const [q, setQ] = useState('');
return <input value={q} onChange={(e) => setQ(e.target.value)} />;
}
// app/users/_components/UserList.tsx — RSC (no state, no events)
import type { User } from '@/types';
export function UserList({ users }: { users: User[] }) {
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
The page stays RSC, fetches data on the server, and only the interactive UserFilter is Client.
children// app/(app)/layout.tsx — RSC
import { ClientShell } from './_components/ClientShell';
import { ServerSideNav } from './_components/ServerSideNav';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ClientShell>
<ServerSideNav /> {/* Server, passed as part of children */}
{children} {/* Server, passed through */}
</ClientShell>
);
}
// _components/ClientShell.tsx — Client
'use client';
import { useState } from 'react';
export function ClientShell({ children }: { children: React.ReactNode }) {
const [sidebar, setSidebar] = useState(false);
return <div>{sidebar && <aside />}{children}</div>;
}
The Client Component receives the Server Component as part of children. It cannot directly import a Server Component.
// ❌ This breaks — Client Component cannot import RSC
'use client';
import { ServerOnlyThing } from './ServerOnlyThing'; // Will fail at build
If ServerOnlyThing does NOT use server-only modules and is just rendering data, it can be made client-compatible (no "use client" needed — leave as RSC, but only pass via children/props from a Server parent).
When a Server Component renders a Client Component, props are SERIALIZED:
Date, Map, Set, BigInt.Decimal from Prisma — convert to string/number), Symbols.// ❌ Class instance — won't serialize
const user = await prisma.user.findFirst();
return <ClientComponent balance={user.balance} />; // balance is Decimal — fails
// ✅ Convert at the boundary
return <ClientComponent balance={user.balance.toNumber()} />;
For Prisma Decimal, BigInt, etc., serialize before passing.
"use server")A function callable from anywhere (Client OR Server) but executes on the server.
// app/users/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1) });
export async function createUser(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error('unauthorized');
const parsed = CreateUserSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { ok: false as const, error: parsed.error.flatten() };
}
await db.users.create({ data: parsed.data });
revalidatePath('/users');
redirect('/users');
}
// app/users/page.tsx
export default async function UsersPage() {
async function deleteUser(id: string) {
'use server';
const session = await auth();
if (!session?.user) throw new Error('unauthorized');
await db.users.delete({ where: { id } });
revalidatePath('/users');
}
return <UserList onDelete={deleteUser} />;
}
// _components/CreateUserForm.tsx
'use client';
import { createUser } from '../actions';
import { useFormState } from 'react-dom';
export function CreateUserForm() {
const [state, formAction] = useFormState(createUser, null);
return (
<form action={formAction}>
<input name="email" />
<input name="name" />
<button>Create</button>
{state?.ok === false && <p>{JSON.stringify(state.error)}</p>}
</form>
);
}
Or directly from event handlers:
'use client';
import { deleteUser } from '../actions';
export function DeleteButton({ id }: { id: string }) {
return <button onClick={() => deleteUser(id)}>Delete</button>;
}
revalidatePath / revalidateTag after mutations — otherwise the client sees stale data.serverActions.allowedOrigins in next.config.js for production CORS-like protection.useFormStatus and useFormState'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
useFormStatus MUST be inside a <form action={...}>. Pairs with useFormState for return-value propagation.
RSC + <Suspense> enables streaming:
import { Suspense } from 'react';
export default function Page() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading users...</div>}>
<UsersList /> {/* async RSC */}
</Suspense>
<Suspense fallback={<div>Loading orders...</div>}>
<OrdersList /> {/* async RSC */}
</Suspense>
</>
);
}
The shell (<h1>) renders immediately; users and orders stream in independently as their data arrives. Pair with loading.tsx for the segment-level fallback.
import { cookies, headers } from 'next/headers';
export default async function Page() {
const cookieStore = cookies();
const token = cookieStore.get('session')?.value;
// ...
}
In Next.js 15+, cookies() and headers() return Promises — await them.
"use client" at the top of every file by reflex. Default is RSC.revalidatePath after mutations — UI shows stale data.fs, ORM clients) in files that ARE OR MIGHT BECOME Client. Use import 'server-only' at the top of files that must remain server-only — it'll error if accidentally imported into client.<Suspense> without an async child — there's nothing to suspend on.useEffect to fetch data in a Client Component when the parent could fetch it as RSC and pass it down.npx claudepluginhub aratkruglik/claude-sdlc --plugin nextjs-pluginGuides React Server Components patterns for Next.js 16: server vs client boundaries, async components, data fetching, serialization rules, streaming with Suspense. Use when deciding component boundaries or streaming UI.
Guides using Next.js Server Components for data fetching, direct DB access, Server vs Client decisions, and Server Actions in data-intensive apps.
Enables React Server Components to eliminate client JavaScript and directly access data in server components. Provides patterns for splitting server and client components using 'use client' directive.