# React Server Components

> When to activate: React Server Components, RSC, client boundary, streaming, Suspense, data fetching in RSC, server actions

- Skill: `mattakushi432/react-server-components` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/react-server-components`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/react-server-components/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/react-server-components

---


# React Server Components Patterns

## Mental Model

```
┌─────────────────────────────────────────────┐
│  Server                                      │
│  ┌────────────────────────────────────────┐  │
│  │  RSC Tree (async, no hooks, no events) │  │
│  │                                         │  │
│  │   <Page>            ← Server           │  │
│  │     <Header>        ← Server           │  │
│  │     <ProductList>   ← Server (DB call) │  │
│  │       <AddToCart>   ← 'use client' ─── │──┼─→ Client bundle
│  │     <Footer>        ← Server           │  │
│  └────────────────────────────────────────┘  │
└─────────────────────────────────────────────┘
```

**Server Components can:**
- `async/await` directly
- Access databases, file system, env vars
- Import server-only packages

**Server Components cannot:**
- Use `useState`, `useEffect`, event handlers
- Access browser APIs
- Be passed callbacks as props

## Data Fetching in RSC
```tsx
// app/products/page.tsx — Server Component
async function ProductsPage({ searchParams }: { searchParams: { q?: string } }) {
  // Direct DB access — no API layer needed
  const products = await prisma.product.findMany({
    where: searchParams.q
      ? { name: { contains: searchParams.q, mode: 'insensitive' } }
      : undefined,
    include: { category: true },
    orderBy: { createdAt: 'desc' },
  })

  return (
    <main>
      <h1>Products</h1>
      <Suspense fallback={<ProductListSkeleton count={products.length} />}>
        <ProductList products={products} />
      </Suspense>
    </main>
  )
}
```

## Parallel Data Fetching
```tsx
// DO: fetch in parallel when data is independent
async function DashboardPage() {
  const [user, analytics, notifications] = await Promise.all([
    getUser(),
    getAnalytics(),
    getNotifications(),
  ])

  return (
    <div>
      <Header user={user} />
      <Analytics data={analytics} />
      <NotificationBell notifications={notifications} />
    </div>
  )
}

// DO: initiate fetches early (Request Waterfall mitigation)
async function ProductPage({ params }: { params: { id: string } }) {
  const productPromise     = getProduct(params.id)
  const recommendedPromise = getRecommended(params.id)  // starts in parallel

  const product = await productPromise  // await only when needed

  return (
    <div>
      <ProductDetail product={product} />
      <Suspense fallback={<Skeleton />}>
        <Recommended promise={recommendedPromise} />
      </Suspense>
    </div>
  )
}
```

## Client Boundary
```tsx
// Push the 'use client' boundary as deep as possible

// ❌ WRONG: entire section becomes client bundle
'use client'
async function ProductList({ products }: { products: Product[] }) {
  // now you can't use await here anyway
}

// ✓ CORRECT: only interactive leaf is a Client Component
async function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>
          {p.name}
          <AddToCartButton productId={p.id} />  {/* 'use client' */}
        </li>
      ))}
    </ul>
  )
}
```

## Passing Data Through Boundaries
```tsx
// Serializable props pass the boundary — RSC serializes them
<ClientComponent
  id={product.id}          // ✓ string
  name={product.name}      // ✓ string
  price={product.price}    // ✓ number
  tags={product.tags}      // ✓ string[]
  createdAt={product.createdAt} // ✓ Date (serialized)
/>

// Functions cannot pass the boundary from Server → Client
// Use Server Actions instead
```

## Server Actions in Forms
```tsx
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

const CreatePostSchema = z.object({
  title:   z.string().min(1).max(200),
  content: z.string().min(10),
})

export async function createPost(prevState: any, formData: FormData) {
  const parsed = CreatePostSchema.safeParse({
    title:   formData.get('title'),
    content: formData.get('content'),
  })

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors }
  }

  const post = await db.post.create({ data: parsed.data })
  revalidatePath('/posts')
  redirect(`/posts/${post.id}`)
}
```

```tsx
// Client Component using the action
'use client'
import { useActionState } from 'react'
import { createPost } from '../actions'

function CreatePostForm() {
  const [state, action, isPending] = useActionState(createPost, null)

  return (
    <form action={action}>
      <input name="title" required />
      {state?.errors?.title && <span>{state.errors.title}</span>}
      <textarea name="content" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  )
}
```

## Caching & Revalidation
```tsx
// Per-request cache (deduplicate within a render)
import { cache } from 'react'

const getUser = cache(async (id: string) => {
  return prisma.user.findUnique({ where: { id } })
})

// Both calls hit DB only once per request
const user1 = await getUser('123')
const user2 = await getUser('123')  // cache hit

// Next.js: tag-based cache invalidation
const post = await fetch(`/api/posts/${id}`, {
  next: { tags: [`post:${id}`] }
})

// Invalidate from Server Action:
revalidateTag(`post:${id}`)
```

## Server-Only Utilities
```ts
// lib/db.server.ts — cannot be imported in Client Components
import 'server-only'  // throws at build if imported in client bundle
import { PrismaClient } from '@prisma/client'
export const db = new PrismaClient()
```

