
React Impl Server Components
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-impl-server-components is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-impl-server-components
- Frontend Development
- AI-coding skill
React Impl Server Components by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-impl-server-componentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-impl-server-components
Quick Reference
Framework Requirement
NEVER attempt to use Server Components without a supporting framework. React Server Components require a bundler and server integration — standalone React (create-react-app, plain Vite) does NOT support them. Use Next.js App Router (13.4+), or another RSC-compatible framework.
Server vs Client Component Rules
| Capability | Server Component | Client Component |
|---|---|---|
| Directive needed | NONE (default) | "use client" at top of file |
async/await in render | YES | NO |
| Direct DB/filesystem access | YES | NO |
useState / useReducer | NO | YES |
useEffect / useLayoutEffect | NO | YES |
Event handlers (onClick, onChange) | NO | YES |
Browser APIs (localStorage, window) | NO | YES |
useContext / use(Context) | NO | YES |
| Import Client Components | YES | YES |
| Import Server Components | YES | NO |
Receive Server Component as children | N/A | YES |
Directive Rules
- Server Components: NO directive needed — they are the DEFAULT in RSC frameworks
- Client Components: MUST have
"use client"as the FIRST line of the file - Server Functions: Use
"use server"— either inline in a Server Component function body, or at the top of a dedicated file "use server"marks Server Functions, NOT Server Components — NEVER use it to "mark" a component as server-side
Serialization Rules (Server-to-Client Boundary)
| Data Type | Can Cross Boundary? | Notes |
|---|---|---|
string, number, boolean | YES | Primitive values serialize directly |
null, undefined | YES | |
| Plain objects / arrays | YES | Values must themselves be serializable |
Date | YES | Serialized as ISO string |
Promise<T> | YES | Consumed via use() on client (React 19) |
| JSX elements | YES | Pre-rendered output, not source code |
| Server Functions | YES | Sent as serializable references, not code |
| Regular functions | NO | NEVER pass callbacks across the boundary |
| Class instances | NO | Not serializable |
| DOM nodes | NO | Server has no DOM |
Symbol | NO | Not serializable |
| Database connections | NO | Server-only resources |
| Secrets / API keys | NO | NEVER expose to client |
Critical Warnings
NEVER import a Server Component inside a Client Component file — the bundler treats everything imported from a "use client" file as client code. ALWAYS pass Server Component output as children or other JSX props instead.
NEVER pass regular functions as props from Server to Client Components — only Server Functions (marked with "use server") can cross the boundary. All other functions are not serializable.
NEVER use "use server" at the top of a component file thinking it makes the component a Server Component — Server Components need NO directive. "use server" is exclusively for Server Functions.
ALWAYS wrap async Server Components in <Suspense> boundaries when they perform data fetching — without Suspense, the entire page blocks until the data resolves.
ALWAYS use a supporting framework (Next.js App Router 13.4+) — React alone does NOT provide the server infrastructure for RSC.
---
Decision Tree: Server or Client Component?
Does this component need useState, useEffect, useReducer, or other hooks?
├─ YES → Client Component ("use client")
│
Does this component need event handlers (onClick, onChange, onSubmit)?
├─ YES → Client Component ("use client")
│
Does this component need browser APIs (localStorage, window, navigator)?
├─ YES → Client Component ("use client")
│
Does this component fetch data from a database or filesystem?
├─ YES → Server Component (default, no directive)
│
Does this component use heavy libraries only needed for rendering (markdown, syntax highlighting)?
├─ YES → Server Component (zero client bundle cost)
│
Is this a layout, page, or data-fetching wrapper?
├─ YES → Server Component (default)
│
Is this a leaf component with no interactivity?
├─ YES → Server Component (default)
│
Unsure?
└─ Start as Server Component. Add "use client" ONLY when you need client features.---
Server Components: Core Patterns
Async Data Fetching (Server Component)
// app/notes/[id]/page.tsx — Server Component (no directive)
import { Suspense } from 'react';
import { NoteViewer } from './NoteViewer';
import { CommentList } from './CommentList';
import { db } from '@/lib/db';
interface PageProps {
params: { id: string };
}
export default async function NotePage({ params }: PageProps) {
const note = await db.notes.findUnique({ where: { id: params.id } });
if (!note) return <p>Note not found</p>;
// Start promise on server, stream to client
const commentsPromise = db.comments.findMany({ where: { noteId: note.id } });
return (
<article>
<h1>{note.title}</h1>
<NoteViewer content={note.content} />
<Suspense fallback={<p>Loading comments...</p>}>
<CommentList commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}Server-to-Client Data Streaming (React 19)
// CommentList.tsx — Client Component consuming server promise
"use client";
import { use } from 'react';
interface Comment {
id: string;
text: string;
author: string;
}
export function CommentList({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise); // suspends until resolved
return (
<ul>
{comments.map(c => (
<li key={c.id}>{c.author}: {c.text}</li>
))}
</ul>
);
}Bundle Size Optimization
// Server Component — these libraries NEVER ship to the client
import { marked } from 'marked'; // 35.9K gzipped
import sanitizeHtml from 'sanitize-html'; // 63.3K gzipped
export default async function MarkdownPage({ slug }: { slug: string }) {
const content = await fs.readFile(`content/${slug}.md`, 'utf8');
return <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(marked(content)) }} />;
}---
Server Functions (Server Actions)
Terminology
- Server Function: ANY
asyncfunction marked with"use server" - Server Action: A Server Function used as a form
actionor called from an Action context
Creating Server Functions
Method 1: Inline in Server Component
// Server Component
import { SubmitButton } from './SubmitButton';
export default function NewNote() {
async function createNote(formData: FormData) {
'use server';
const title = formData.get('title') as string;
await db.notes.create({ data: { title } });
}
return (
<form action={createNote}>
<input name="title" required />
<SubmitButton />
</form>
);
}Method 2: Dedicated File (importable by Client Components)
// actions.ts
"use server";
export async function createNote(formData: FormData): Promise<{ error?: string }> {
const title = formData.get('title') as string;
if (!title) return { error: 'Title is required' };
await db.notes.create({ data: { title } });
return {};
}
export async function deleteNote(noteId: string): Promise<void> {
await db.notes.delete({ where: { id: noteId } });
}Form Integration with Progressive Enhancement
// ClientForm.tsx
"use client";
import { useActionState } from 'react';
import { createNote } from './actions';
export function CreateNoteForm() {
const [state, submitAction, isPending] = useActionState(createNote, { error: undefined });
return (
<form action={submitAction}>
<input name="title" disabled={isPending} required />
{state.error && <p className="error">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Note'}
</button>
</form>
);
}Forms with Server Actions work without JavaScript — the browser submits the form as a standard POST request, and the server processes it. This is progressive enhancement.
---
Composition: Server + Client Components
Pattern: Server Component Wrapping Client Component
// Server Component (layout or page)
import { Sidebar } from './Sidebar'; // Client Component
import { db } from '@/lib/db';
export default async function Dashboard() {
const user = await db.users.getCurrent();
const navItems = await db.nav.getForUser(user.id);
return (
<div className="dashboard">
<Sidebar items={navItems} userName={user.name} />
<main>{/* more server-rendered content */}</main>
</div>
);
}Pattern: Passing Server Content as Children to Client Component
// Server Component
import { Expandable } from './Expandable'; // Client Component
import { db } from '@/lib/db';
export default async function NoteList() {
const notes = await db.notes.findMany();
return (
<div>
{notes.map(note => (
<Expandable key={note.id} title={note.title}>
<p>{note.content}</p> {/* Server-rendered, passed as children */}
</Expandable>
))}
</div>
);
}// Expandable.tsx — Client Component
"use client";
import { useState, type ReactNode } from 'react';
export function Expandable({ title, children }: { title: string; children: ReactNode }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button onClick={() => setExpanded(!expanded)}>{title}</button>
{expanded && children}
</div>
);
}The browser NEVER receives Server Component source code — only the pre-rendered HTML/JSX output and Client Component code for hydration.
---
React 18 vs React 19 Compatibility
| Feature | React 18 | React 19 |
|---|---|---|
| Server Components | Canary only (experimental) | Stable |
"use client" directive | Canary only | Stable |
"use server" directive | Canary only | Stable |
use() for promises | NOT available | Stable |
useActionState | NOT available (useFormState in canary) | Stable |
| Server Function return types | Limited | Full serializable support |
| Streaming with Suspense | Available via renderToPipeableStream | Enhanced with RSC integration |
ALWAYS target React 19 for Server Component projects. React 18 support was experimental (canary) and is NOT recommended for production RSC usage.
---
Reference Links
- references/examples.md -- Complete Server Component patterns with working code
- references/patterns.md -- Server/Client boundary composition patterns
- references/anti-patterns.md -- Common RSC mistakes and how to avoid them
Official Sources
- https://react.dev/reference/rsc/server-components
- https://react.dev/reference/rsc/server-functions
- https://react.dev/reference/react/use
- https://react.dev/reference/react/useActionState
- https://react.dev/blog/2024/12/05/react-19
Server Component Anti-Patterns
Reference file for react-impl-server-components skill.Common mistakes when working with React Server Components, with explanations and corrections.
---
Anti-Pattern 1: Importing Server Components Inside Client Components
WRONG
// Dashboard.tsx — Client Component
"use client";
import { UserStats } from './UserStats'; // UserStats is a Server Component
export function Dashboard() {
const [tab, setTab] = useState('stats');
return (
<div>
<button onClick={() => setTab('stats')}>Stats</button>
{tab === 'stats' && <UserStats />} {/* BROKEN: UserStats becomes a Client Component */}
</div>
);
}WHY IT FAILS
When a "use client" file imports another module, the bundler treats that module as client code. The Server Component loses all server capabilities — async/await, database access, filesystem access — and WILL error at runtime or silently become a client-only component.
CORRECT
// Dashboard.tsx — Client Component
"use client";
import { useState, type ReactNode } from 'react';
export function Dashboard({ statsSlot }: { statsSlot: ReactNode }) {
const [tab, setTab] = useState('stats');
return (
<div>
<button onClick={() => setTab('stats')}>Stats</button>
{tab === 'stats' && statsSlot}
</div>
);
}
// page.tsx — Server Component
import { Dashboard } from './Dashboard';
import { UserStats } from './UserStats'; // Server Component, imported from server context
export default async function Page() {
return <Dashboard statsSlot={<UserStats />} />;
}Rule: ALWAYS pass Server Component output as children or named ReactNode props from a Server Component parent. NEVER import Server Components directly in Client Component files.
---
Anti-Pattern 2: Using "use server" to Mark Server Components
WRONG
// UserProfile.tsx
"use server"; // WRONG — this marks the file's exports as Server Functions
export default async function UserProfile() {
const user = await db.users.getCurrent();
return <div>{user.name}</div>;
}WHY IT FAILS
"use server" makes every exported function a Server Function (callable from the client via RPC). It does NOT mark the file as a Server Component. The component will NOT render as expected — the framework treats it as a callable function, not a renderable component.
CORRECT
// UserProfile.tsx — Server Component (NO directive needed)
export default async function UserProfile() {
const user = await db.users.getCurrent();
return <div>{user.name}</div>;
}Rule: Server Components are the DEFAULT — they need NO directive. "use server" is EXCLUSIVELY for Server Functions.
---
Anti-Pattern 3: Passing Non-Serializable Props Across the Boundary
WRONG
// Server Component
import { DataTable } from './DataTable'; // Client Component
export default async function Page() {
const formatDate = (d: Date) => d.toLocaleDateString();
return (
<DataTable
data={rows}
formatDate={formatDate} // FAILS: regular functions are not serializable
dbConnection={db} // FAILS: class instance
renderRow={(row) => <tr><td>{row.name}</td></tr>} // FAILS: function
/>
);
}WHY IT FAILS
Only serializable values can cross the server-to-client boundary. Regular functions, class instances, Symbols, and DOM nodes cannot be serialized. The framework throws a serialization error at build time or runtime.
CORRECT
// Server Component
import { DataTable } from './DataTable';
export default async function Page() {
const rows = await db.query('SELECT * FROM items');
return (
<DataTable
data={rows} // plain array of objects — serializable
dateFormat="en-US" // pass configuration, not functions
columns={['name', 'date', 'status']} // serializable descriptor
/>
);
}// DataTable.tsx — Client Component
"use client";
export function DataTable({ data, dateFormat, columns }: Props) {
// Format logic lives in the Client Component
const formatDate = (d: string) => new Date(d).toLocaleDateString(dateFormat);
return (
<table>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={col}>{col === 'date' ? formatDate(row[col]) : row[col]}</td>
))}
</tr>
))}
</tbody>
</table>
);
}Rule: ALWAYS move formatting and rendering logic into the Client Component. Pass only serializable data and configuration strings/numbers/booleans across the boundary.
---
Anti-Pattern 4: Using Hooks in Server Components
WRONG
// Server Component — NO directive
import { useState, useEffect } from 'react';
export default function Settings() {
const [theme, setTheme] = useState('light'); // FAILS: hooks require client
useEffect(() => {
document.body.className = theme;
}, [theme]);
return <div>Settings page</div>;
}WHY IT FAILS
Server Components run on the server where there is no persistent component instance, no re-rendering, and no DOM. useState, useEffect, useReducer, useRef, and other stateful hooks CANNOT work in this environment.
CORRECT
Split into a Server Component for data and a Client Component for interactivity:
// page.tsx — Server Component
import { ThemeSettings } from './ThemeSettings';
import { getUserPreferences } from '@/lib/db';
export default async function SettingsPage() {
const prefs = await getUserPreferences();
return <ThemeSettings initialTheme={prefs.theme} />;
}// ThemeSettings.tsx — Client Component
"use client";
import { useState, useEffect } from 'react';
export function ThemeSettings({ initialTheme }: { initialTheme: string }) {
const [theme, setTheme] = useState(initialTheme);
useEffect(() => {
document.body.className = theme;
}, [theme]);
return (
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
);
}Rule: If a component needs ANY hook (useState, useEffect, useReducer, useRef, useContext), it MUST be a Client Component with "use client".
---
Anti-Pattern 5: Using Server Components Without a Framework
WRONG
// In a plain Vite + React app (NO Next.js or RSC framework)
// src/components/ServerPage.tsx
export default async function ServerPage() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return <div>{json.title}</div>; // FAILS: plain React does not support async components
}WHY IT FAILS
React Server Components require a framework that provides: 1. A server runtime to execute Server Components 2. A bundler that understands "use client" and "use server" directives 3. A streaming protocol to send RSC payload to the client 4. Client-side hydration that stitches Server and Client Components together
Plain React (Vite, CRA, Parcel) provides NONE of these.
CORRECT
Use Next.js App Router (13.4+) or another RSC-compatible framework:
npx create-next-app@latest my-app --appThen async Server Components work in the app/ directory by default.
Rule: NEVER attempt to use Server Components in a plain React project. ALWAYS use Next.js App Router or another framework that implements the RSC protocol.
---
Anti-Pattern 6: Data Fetching in Client Components When Server Components Suffice
WRONG
// UserList.tsx — Client Component (unnecessarily)
"use client";
import { useState, useEffect } from 'react';
export function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => { setUsers(data); setLoading(false); });
}, []);
if (loading) return <p>Loading...</p>;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}WHY IT IS SUBOPTIMAL
This pattern: 1. Ships the component code to the client (bundle size) 2. Requires an API route as a middleman 3. Creates a waterfall: HTML loads -> JS loads -> fetch fires -> data renders 4. Shows a loading spinner while the server already has the data
CORRECT
// UserList.tsx — Server Component
import { db } from '@/lib/db';
export default async function UserList() {
const users = await db.users.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}This pattern: 1. Zero client JavaScript for this component 2. No API route needed — direct database access 3. No waterfall — data is fetched and rendered on the server in one step 4. HTML arrives with data already rendered
Rule: ALWAYS use Server Components for data display that requires no interactivity. Reserve Client Components for components that NEED hooks, event handlers, or browser APIs.
---
Anti-Pattern 7: Exposing Secrets Through Server Functions
WRONG
// actions.ts
"use server";
export async function getData() {
// API key is safe on server, but...
const res = await fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const data = await res.json();
return {
items: data.items,
apiKey: process.env.API_KEY, // EXPOSED: returned to client
dbUrl: process.env.DATABASE_URL, // EXPOSED: returned to client
};
}WHY IT FAILS
Server Functions execute on the server, but their RETURN VALUES are sent to the client. Any secret included in the return value is exposed to the browser.
CORRECT
// actions.ts
"use server";
export async function getData() {
const res = await fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const data = await res.json();
return {
items: data.items, // ONLY return what the client needs
};
}Rule: NEVER include environment variables, API keys, database URLs, or other secrets in Server Function return values. ALWAYS return only the data the client needs to render.
---
Anti-Pattern 8: Missing Suspense Around Async Server Components
WRONG
// page.tsx — Server Component
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<SlowDataComponent /> {/* No Suspense — entire page blocks */}
<FastContent /> {/* This also waits, even though it is fast */}
</main>
);
}
async function SlowDataComponent() {
const data = await slowApiCall(); // 3 seconds
return <div>{data.summary}</div>;
}WHY IT IS SUBOPTIMAL
Without Suspense, the entire page waits for ALL async Server Components to resolve before ANY content is sent to the browser. The user sees nothing for 3 seconds.
CORRECT
// page.tsx — Server Component
import { Suspense } from 'react';
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<FastContent /> {/* Renders immediately */}
<Suspense fallback={<p>Loading data...</p>}>
<SlowDataComponent /> {/* Streams in when ready */}
</Suspense>
</main>
);
}Rule: ALWAYS wrap async Server Components that perform slow data fetching in <Suspense> boundaries. This enables streaming — fast content renders immediately while slow content shows a fallback.
---
Anti-Pattern Summary Table
| Anti-Pattern | Symptom | Fix |
|---|---|---|
| Import Server in Client | Server Component loses async capabilities | Pass as children or ReactNode prop |
"use server" on component | Component treated as RPC function | Remove directive (server is default) |
| Non-serializable props | Serialization error at boundary | Pass data/config, move logic to client |
| Hooks in Server Component | Runtime error (hooks undefined) | Split into Server + Client Components |
| RSC without framework | Async components fail to render | Use Next.js App Router or equivalent |
| Client fetch when server suffices | Unnecessary waterfall and bundle | Use async Server Component instead |
| Secrets in return values | API keys exposed to browser | Return only client-needed data |
| Missing Suspense | Entire page blocks on slow data | Wrap async components in Suspense |
Server Component Examples
Reference file for react-impl-server-components skill.All examples use TypeScript/TSX and target React 19 with Next.js App Router.
---
Example 1: Full-Stack CRUD with Server Components and Server Actions
Database Layer (Server-Only)
// lib/db.ts — Server-only module
import { prisma } from './prisma';
export interface Note {
id: string;
title: string;
content: string;
createdAt: Date;
updatedAt: Date;
}
export async function getNotes(): Promise<Note[]> {
return prisma.note.findMany({ orderBy: { updatedAt: 'desc' } });
}
export async function getNote(id: string): Promise<Note | null> {
return prisma.note.findUnique({ where: { id } });
}
export async function createNote(title: string, content: string): Promise<Note> {
return prisma.note.create({ data: { title, content } });
}
export async function deleteNote(id: string): Promise<void> {
await prisma.note.delete({ where: { id } });
}Server Actions File
// app/notes/actions.ts
"use server";
import { revalidatePath } from 'next/cache';
import { createNote, deleteNote } from '@/lib/db';
interface ActionState {
error?: string;
success?: boolean;
}
export async function createNoteAction(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
if (!title?.trim()) return { error: 'Title is required' };
if (!content?.trim()) return { error: 'Content is required' };
try {
await createNote(title.trim(), content.trim());
revalidatePath('/notes');
return { success: true };
} catch {
return { error: 'Failed to create note' };
}
}
export async function deleteNoteAction(noteId: string): Promise<ActionState> {
try {
await deleteNote(noteId);
revalidatePath('/notes');
return { success: true };
} catch {
return { error: 'Failed to delete note' };
}
}Server Component (Page)
// app/notes/page.tsx — Server Component
import { Suspense } from 'react';
import { getNotes } from '@/lib/db';
import { CreateNoteForm } from './CreateNoteForm';
import { NoteCard } from './NoteCard';
export default async function NotesPage() {
const notes = await getNotes();
return (
<main>
<h1>Notes</h1>
<CreateNoteForm />
<Suspense fallback={<p>Loading notes...</p>}>
<section>
{notes.map(note => (
<NoteCard key={note.id} note={note} />
))}
{notes.length === 0 && <p>No notes yet.</p>}
</section>
</Suspense>
</main>
);
}Client Component (Form with useActionState)
// app/notes/CreateNoteForm.tsx
"use client";
import { useActionState } from 'react';
import { createNoteAction } from './actions';
export function CreateNoteForm() {
const [state, submitAction, isPending] = useActionState(createNoteAction, {});
return (
<form action={submitAction}>
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" required disabled={isPending} />
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" required disabled={isPending} />
</div>
{state.error && <p role="alert" className="error">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Note'}
</button>
</form>
);
}Client Component (Delete with useTransition)
// app/notes/NoteCard.tsx
"use client";
import { useTransition } from 'react';
import { deleteNoteAction } from './actions';
import type { Note } from '@/lib/db';
export function NoteCard({ note }: { note: Note }) {
const [isPending, startTransition] = useTransition();
function handleDelete() {
startTransition(async () => {
await deleteNoteAction(note.id);
});
}
return (
<article style={{ opacity: isPending ? 0.5 : 1 }}>
<h2>{note.title}</h2>
<p>{note.content}</p>
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
</article>
);
}---
Example 2: Parallel Data Fetching with Suspense
// app/dashboard/page.tsx — Server Component
import { Suspense } from 'react';
import { UserProfile } from './UserProfile';
import { RecentActivity } from './RecentActivity';
import { Statistics } from './Statistics';
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<div className="grid">
{/* Each Suspense boundary streams independently */}
<Suspense fallback={<div className="skeleton">Loading profile...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div className="skeleton">Loading activity...</div>}>
<RecentActivity />
</Suspense>
<Suspense fallback={<div className="skeleton">Loading stats...</div>}>
<Statistics />
</Suspense>
</div>
</main>
);
}// app/dashboard/UserProfile.tsx — Server Component
import { getCurrentUser } from '@/lib/auth';
export async function UserProfile() {
const user = await getCurrentUser(); // fetches on server
return (
<section>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Member since {user.createdAt.toLocaleDateString()}</p>
</section>
);
}// app/dashboard/Statistics.tsx — Server Component
import { getStats } from '@/lib/analytics';
export async function Statistics() {
const stats = await getStats(); // independent fetch, streams when ready
return (
<section>
<h2>This Month</h2>
<dl>
<dt>Page Views</dt><dd>{stats.pageViews.toLocaleString()}</dd>
<dt>Users</dt><dd>{stats.activeUsers.toLocaleString()}</dd>
<dt>Revenue</dt><dd>${stats.revenue.toFixed(2)}</dd>
</dl>
</section>
);
}---
Example 3: Server Component with Promise Streaming to Client
// app/products/[id]/page.tsx — Server Component
import { Suspense } from 'react';
import { getProduct, getReviews } from '@/lib/db';
import { AddToCartButton } from './AddToCartButton';
import { ReviewList } from './ReviewList';
export default async function ProductPage({ params }: { params: { id: string } }) {
// Await critical data (blocks render)
const product = await getProduct(params.id);
if (!product) return <p>Product not found</p>;
// Start non-critical data as promise (streams to client)
const reviewsPromise = getReviews(product.id);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p className="price">${product.price.toFixed(2)}</p>
<AddToCartButton productId={product.id} />
<Suspense fallback={<p>Loading reviews...</p>}>
<ReviewList reviewsPromise={reviewsPromise} />
</Suspense>
</main>
);
}// app/products/[id]/ReviewList.tsx — Client Component
"use client";
import { use } from 'react';
interface Review {
id: string;
author: string;
rating: number;
text: string;
}
export function ReviewList({ reviewsPromise }: { reviewsPromise: Promise<Review[]> }) {
const reviews = use(reviewsPromise);
if (reviews.length === 0) return <p>No reviews yet.</p>;
return (
<section>
<h2>Reviews ({reviews.length})</h2>
{reviews.map(review => (
<article key={review.id}>
<strong>{review.author}</strong>
<span>{'★'.repeat(review.rating)}{'☆'.repeat(5 - review.rating)}</span>
<p>{review.text}</p>
</article>
))}
</section>
);
}---
Example 4: Optimistic UI with Server Actions
// app/todos/actions.ts
"use server";
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function addTodo(text: string): Promise<{ id: string; text: string }> {
const todo = await db.todo.create({ data: { text, completed: false } });
revalidatePath('/todos');
return { id: todo.id, text: todo.text };
}
export async function toggleTodo(id: string): Promise<void> {
const todo = await db.todo.findUnique({ where: { id } });
if (todo) {
await db.todo.update({ where: { id }, data: { completed: !todo.completed } });
}
revalidatePath('/todos');
}// app/todos/TodoList.tsx — Client Component with optimistic updates
"use client";
import { useOptimistic, useTransition } from 'react';
import { addTodo, toggleTodo } from './actions';
interface Todo {
id: string;
text: string;
completed: boolean;
pending?: boolean;
}
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [isPending, startTransition] = useTransition();
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
initialTodos,
(currentTodos: Todo[], action: { type: 'add'; text: string } | { type: 'toggle'; id: string }) => {
if (action.type === 'add') {
return [...currentTodos, { id: crypto.randomUUID(), text: action.text, completed: false, pending: true }];
}
if (action.type === 'toggle') {
return currentTodos.map(t =>
t.id === action.id ? { ...t, completed: !t.completed, pending: true } : t
);
}
return currentTodos;
}
);
function handleAdd(formData: FormData) {
const text = formData.get('text') as string;
if (!text.trim()) return;
startTransition(async () => {
setOptimisticTodos({ type: 'add', text });
await addTodo(text);
});
}
function handleToggle(id: string) {
startTransition(async () => {
setOptimisticTodos({ type: 'toggle', id });
await toggleTodo(id);
});
}
return (
<div>
<form action={handleAdd}>
<input name="text" placeholder="New todo..." required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</label>
</li>
))}
</ul>
</div>
);
}---
Example 5: Layout with Mixed Server/Client Components
// app/layout.tsx — Server Component (Root Layout)
import { Navigation } from './Navigation';
import { Footer } from './Footer';
import { ThemeProvider } from './ThemeProvider';
import { getCurrentUser } from '@/lib/auth';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser();
return (
<html lang="en">
<body>
<ThemeProvider>
<Navigation user={user} />
{children}
<Footer />
</ThemeProvider>
</body>
</html>
);
}// app/ThemeProvider.tsx — Client Component (context requires client)
"use client";
import { createContext, useState, type ReactNode } from 'react';
export const ThemeContext = createContext<{ theme: string; toggle: () => void }>({
theme: 'light',
toggle: () => {},
});
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState('light');
const toggle = () => setTheme(t => (t === 'light' ? 'dark' : 'light'));
return (
<ThemeContext value={{ theme, toggle }}>
<div data-theme={theme}>{children}</div>
</ThemeContext>
);
}// app/Navigation.tsx — Client Component (needs onClick for mobile menu)
"use client";
import { useState } from 'react';
interface User {
name: string;
avatarUrl: string;
}
export function Navigation({ user }: { user: User | null }) {
const [menuOpen, setMenuOpen] = useState(false);
return (
<nav>
<a href="/">Home</a>
<button onClick={() => setMenuOpen(!menuOpen)} aria-label="Toggle menu">
Menu
</button>
{menuOpen && (
<ul>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/notes">Notes</a></li>
{user && <li>{user.name}</li>}
</ul>
)}
</nav>
);
}// app/Footer.tsx — Server Component (no interactivity needed)
export function Footer() {
const year = new Date().getFullYear();
return (
<footer>
<p>© {year} My App. All rights reserved.</p>
</footer>
);
}Server/Client Boundary Patterns
Reference file for react-impl-server-components skill.Patterns for composing Server and Client Components across the RSC boundary.
---
Pattern 1: Server Parent, Client Child (Most Common)
Server Components ALWAYS render Client Components by importing them. Data flows down as serializable props.
// Server Component
import { InteractiveWidget } from './InteractiveWidget'; // Client Component
export default async function Page() {
const data = await fetchData();
return (
<main>
<h1>{data.title}</h1>
<InteractiveWidget items={data.items} /> {/* serializable props only */}
</main>
);
}Rule: Props passed to Client Components MUST be serializable. NEVER pass functions (except Server Functions), class instances, or DOM nodes.
---
Pattern 2: Client Component Receiving Server Content as Children
When a Client Component needs to wrap server-rendered content, pass it as children. The Server Component pre-renders the children, and the Client Component receives the rendered output.
// Server Component
import { Tabs } from './Tabs'; // Client Component
import { ServerContent } from './ServerContent'; // Server Component
export default async function Page() {
return (
<Tabs labels={['Overview', 'Details']}>
<ServerContent /> {/* Pre-rendered on server, passed as JSX */}
</Tabs>
);
}// Tabs.tsx — Client Component
"use client";
import { useState, type ReactNode } from 'react';
export function Tabs({ labels, children }: { labels: string[]; children: ReactNode }) {
const [active, setActive] = useState(0);
return (
<div>
<div role="tablist">
{labels.map((label, i) => (
<button key={label} role="tab" onClick={() => setActive(i)}>{label}</button>
))}
</div>
<div role="tabpanel">{children}</div>
</div>
);
}Rule: The children prop carries pre-rendered JSX — it crosses the boundary as serialized React elements, NOT as component references.
---
Pattern 3: Slot Pattern (Multiple Server Component Regions in Client)
Pass multiple server-rendered regions as named props to a Client Component.
// Server Component
import { SplitLayout } from './SplitLayout'; // Client Component
import { Sidebar } from './Sidebar'; // Server Component
import { MainContent } from './MainContent'; // Server Component
export default async function Page() {
return (
<SplitLayout
sidebar={<Sidebar />} {/* Server-rendered slot */}
main={<MainContent />} {/* Server-rendered slot */}
/>
);
}// SplitLayout.tsx — Client Component
"use client";
import { useState, type ReactNode } from 'react';
export function SplitLayout({ sidebar, main }: { sidebar: ReactNode; main: ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div className="layout">
{sidebarOpen && <aside>{sidebar}</aside>}
<button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
<main>{main}</main>
</div>
);
}Rule: ALWAYS use ReactNode type for slot props. These carry pre-rendered JSX across the boundary.
---
Pattern 4: Context Provider at the Boundary
Context providers require useState or useReducer, making them Client Components. Place them high in the tree and pass server-rendered children through them.
// app/layout.tsx — Server Component
import { AuthProvider } from './AuthProvider';
import { getCurrentUser } from '@/lib/auth';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser(); // server-side auth check
return (
<html lang="en">
<body>
<AuthProvider initialUser={user}>
{children} {/* Server Components render inside Client provider */}
</AuthProvider>
</body>
</html>
);
}// AuthProvider.tsx — Client Component
"use client";
import { createContext, useState, type ReactNode } from 'react';
interface User { id: string; name: string; email: string; }
export const AuthContext = createContext<{ user: User | null }>({ user: null });
export function AuthProvider({ initialUser, children }: { initialUser: User | null; children: ReactNode }) {
const [user] = useState(initialUser);
return <AuthContext value={{ user }}>{children}</AuthContext>;
}Rule: Fetch data in the Server Component parent, pass it as initialUser prop to the Client provider. NEVER fetch in the Client provider itself.
---
Pattern 5: Server Function Passed to Client Component
Server Functions (marked with "use server") are the ONLY functions that can cross the server-to-client boundary.
// Server Component
import { LikeButton } from './LikeButton';
export default async function Post({ postId }: { postId: string }) {
async function handleLike() {
'use server';
await db.posts.incrementLikes(postId);
}
return (
<article>
<p>Post content...</p>
<LikeButton onLike={handleLike} /> {/* Server Function reference */}
</article>
);
}// LikeButton.tsx — Client Component
"use client";
import { useTransition } from 'react';
export function LikeButton({ onLike }: { onLike: () => Promise<void> }) {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => startTransition(() => onLike())}
disabled={isPending}
>
{isPending ? 'Liking...' : 'Like'}
</button>
);
}Rule: The framework serializes the Server Function as a reference ($$typeof: Symbol.for("react.server.reference")). When the client calls it, the framework sends a request to the server to execute it.
---
Pattern 6: Conditional Rendering at the Boundary
Server Components can conditionally render Client Components based on server-side data.
// Server Component
import { AdminPanel } from './AdminPanel'; // Client Component
import { UserDashboard } from './UserDashboard'; // Client Component
import { getCurrentUser } from '@/lib/auth';
export default async function Page() {
const user = await getCurrentUser();
if (user.role === 'admin') {
return <AdminPanel permissions={user.permissions} />;
}
return <UserDashboard userId={user.id} />;
}Rule: The conditional logic runs on the server. Only the selected Client Component ships to the client — the other is NEVER sent.
---
Pattern 7: Streaming with Nested Suspense Boundaries
Use nested Suspense boundaries to stream different parts of the page independently.
// Server Component
import { Suspense } from 'react';
import { Header } from './Header';
import { Recommendations } from './Recommendations';
import { Comments } from './Comments';
export default async function ArticlePage({ id }: { id: string }) {
const article = await getArticle(id); // blocks — critical content
return (
<main>
<Header title={article.title} />
<article>{article.content}</article>
{/* These stream independently as they resolve */}
<Suspense fallback={<div>Loading recommendations...</div>}>
<Recommendations articleId={id} />
</Suspense>
<Suspense fallback={<div>Loading comments...</div>}>
<Comments articleId={id} />
</Suspense>
</main>
);
}// Recommendations.tsx — Server Component (async)
export async function Recommendations({ articleId }: { articleId: string }) {
const items = await getRecommendations(articleId); // slow API call
return (
<section>
<h2>Recommended</h2>
<ul>{items.map(item => <li key={item.id}>{item.title}</li>)}</ul>
</section>
);
}Rule: Each Suspense boundary creates an independent streaming slot. The shell (everything outside Suspense) renders first, then each boundary fills in as its data resolves.
---
Pattern 8: Error Boundaries with Server Components
Wrap async Server Components in Error Boundaries to handle fetch failures gracefully.
// Server Component
import { Suspense } from 'react';
import { ErrorBoundary } from './ErrorBoundary'; // Client Component
export default function Page() {
return (
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Suspense fallback={<p>Loading...</p>}>
<AsyncServerComponent />
</Suspense>
</ErrorBoundary>
);
}// ErrorBoundary.tsx — Client Component (class component required)
"use client";
import { Component, type ReactNode } from 'react';
interface Props {
fallback: ReactNode;
children: ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
render() {
if (this.state.hasError) return this.props.fallback;
return this.props.children;
}
}Rule: Error Boundaries MUST be Client Components (they use getDerivedStateFromError lifecycle method). ALWAYS place them above async Server Components that might fail.
---
Pattern Summary Table
| Pattern | Use When | Key Rule |
|---|---|---|
| Server Parent, Client Child | Fetching data for interactive UI | Props MUST be serializable |
| Children Prop | Client wrapper around server content | Pass ReactNode, not component references |
| Slot Pattern | Multiple server regions in client layout | Use named ReactNode props |
| Context at Boundary | Shared state with server-fetched initial data | Fetch in server, pass as initial prop |
| Server Function | Client needs to trigger server mutation | ONLY "use server" functions cross boundary |
| Conditional Rendering | Different UI based on server data | Only selected component ships to client |
| Nested Suspense | Independent streaming sections | Each boundary streams independently |
| Error Boundary | Graceful failure handling | Error Boundary MUST be Client Component |