---
title: "React Server Components Framework"
description: "Use when building Next.js 16+ apps with React Server Components. Covers App Router, Cache Components (replacing experimental_ppr), streaming SSR, Server Actions, and React 19 patterns for server-first architecture."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/react-server-components-framework"
---

# React Server Components Framework

Use when building Next.js 16+ apps with React Server Components. Covers App Router, Cache Components (replacing experimental_ppr), streaming SSR, Server Actions, and React 19 patterns for server-first architecture.

<span className="badge badge-gray">Reference</span> <span className="badge badge-yellow">medium</span>

> **Auto-activated** — this skill loads automatically when Claude detects matching context.

<ContextualSkillSidebar slug="react-server-components-framework" />

> **React Server Components Framework** Use when building Next.js 16+ apps with React Server Components. Covers App Router, Cache Components (replacing experimental_ppr), streaming SSR, Server Actions, and React 19 patterns for server-first architecture.


# React Server Components Framework

## Overview

React Server Components (RSC) enable server-first rendering with client-side interactivity. This skill covers Next.js 16.2 LTS App Router patterns, Server Components, Server Actions, and streaming.

> **Next.js 16.2.6 / React 19.2.6 (security release, May 2026)** — Turbopack is the default bundler (no `--turbo` flag needed), Server Fast Refresh is on by default, and the new `cacheComponents` config flag replaces the legacy `experimental_ppr` escape hatch. For AI-agent debugging Next.js ships **Next DevTools MCP** — wire `npx -y next-devtools-mcp@latest` into `.mcp.json` (it connects via the dev server's `/_next/mcp` endpoint) to inspect render trees and cache boundaries mid-session.

**When to use this skill:**
- Building Next.js 16+ applications with the App Router
- Designing component boundaries (Server vs Client Components)
- Implementing data fetching with caching and revalidation
- Creating mutations with Server Actions
- Optimizing performance with streaming and Suspense

---

## Quick Reference

### Server vs Client Components

| Feature | Server Component | Client Component |
|---------|-----------------|------------------|
| Directive | None (default) | `'use client'` |
| Async/await | Yes | No |
| Hooks | No | Yes |
| Browser APIs | No | Yes |
| Database access | Yes | No |
| Client JS bundle | Zero | Ships to client |

**Key Rule**: Server Components can render Client Components, but Client Components cannot directly import Server Components (use `children` prop instead).

### Data Fetching Quick Reference

**Next.js 16 Cache Components (Recommended):**

```tsx
import { cacheLife, cacheTag } from 'next/cache'

// Default — shared across all users (public CDN-cached)
async function CachedProducts() {
  'use cache'
  cacheLife('hours')
  cacheTag('products')
  return await db.product.findMany()
}

// Remote variant (16.2+) — always served from the edge/CDN, never rendered
// inline on the origin. Best for static product listings, marketing content.
async function MarketingHero() {
  'use cache: remote'
  cacheLife('days')
  return <Hero />
}

// Private variant (16.2+) — cached per-user session. Never shared across
// users. Use for personalized dashboards with expensive computation.
async function UserDashboard({ userId }: { userId: string }) {
  'use cache: private'
  cacheLife('minutes')
  cacheTag(`user:${userId}`)
  return await loadDashboard(userId)
}

// Invalidate cache — v16 requires a cacheLife profile as the 2nd arg
import { revalidateTag } from 'next/cache'
revalidateTag('products', 'max') // or updateTag('products') for read-your-writes
```

Enable via `next.config.ts`:

```ts
import type { NextConfig } from 'next'
const config: NextConfig = {
  cacheComponents: true,  // 16.2+ — replaces experimental_ppr flag
}
export default config
```

**Legacy Fetch Options (Next.js 15):**

```tsx
// Static (cached indefinitely)
await fetch(url, { cache: 'force-cache' })

// Revalidate every 60 seconds
await fetch(url, { next: { revalidate: 60 } })

// Always fresh
await fetch(url, { cache: 'no-store' })

// Tag-based revalidation
await fetch(url, { next: { tags: ['posts'] } })
```

### Server Actions Quick Reference

```tsx
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const post = await db.post.create({ data: { title } })
  revalidatePath('/posts')
  redirect("/posts/" + post.id)
}
```

### Async Params/SearchParams (Next.js 16)

Route parameters and search parameters are now Promises that must be awaited:

```tsx
// app/posts/[slug]/page.tsx
export default async function PostPage({
  params,
  searchParams,
}: {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ page?: string }>
}) {
  const { slug } = await params
  const { page } = await searchParams
  return <Post slug={slug} page={page} />
}
```

**Note:** Also applies to `layout.tsx`, `generateMetadata()`, and route handlers. Complete migration guide: first-party `next-upgrade` / `vercel:next-upgrade` skill. House scars: `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/react-server-components-framework/references/ork-delta.md")`.

### Dev Server (Next.js 16.2 LTS)

- **Turbopack default** — `next dev` and `next build` run Turbopack without any flag. Pass `--webpack` only when forced (legacy plugin).
- **Server Fast Refresh** — Server Components hot-reload on save without losing client state. No extra config; it's on by default in 16.2.
- **Next DevTools MCP** — register `npx -y next-devtools-mcp@latest` in `.mcp.json`; it attaches to the running dev server over the `/_next/mcp` endpoint and exposes RSC payloads and cache boundaries to an MCP client. Designed for AI agents that need to inspect render trees mid-session without screenshotting. (There is no `next-browser` binary.)

---

## References

Load on demand with `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/react-server-components-framework/references/&lt;file&gt;")`:
| File | Content |
|------|---------|
| `ork-delta.md` | House rules and scars: fabricated-API corrections from PR #2143, React 19 house conventions (2026-07-31 distillation) |
| `tanstack-router-patterns.md` | React 19 features without Next.js, route-based data fetching, client-rendered app patterns |
| `capability-details.md` | Keyword and problem-mapping metadata for all 12 RSC capabilities |

---

## Upstream coverage (do not restate)

Vendor tutorials for these topics live in first-party skills and docs. This skill keeps only floors, scars, and house decisions (`references/ork-delta.md`).

| Topic | First-party source |
|-------|--------------------|
| Server Components fundamentals (async components, data fetching, route segment config, generateStaticParams, error handling) | `next-best-practices` / `vercel:nextjs` skill; nextjs.org/docs |
| Client Components, `'use client'`, hydration, client-only rendering | `next-best-practices` / `vercel:nextjs` skill |
| Server/Client boundary and composition patterns, serializable props | `next-best-practices` / `vercel:nextjs` skill; `vercel-composition-patterns` |
| Data fetching and caching (fetch cache options, revalidate, tags) | `next-best-practices` / `vercel:nextjs` skill |
| Streaming SSR, Suspense boundaries, loading.tsx, skeleton states | `vercel:nextjs` skill (streaming) |
| Server Actions, progressive enhancement, useActionState forms, Zod validation | `vercel:nextjs` skill (Server Actions) |
| Advanced routing (parallel, intercepting, route groups, dynamic and catch-all) | `vercel:nextjs` skill (routing) |
| Pages Router to App Router migration | `next-upgrade` / `vercel:next-upgrade` skill |
| Next.js 16 upgrade, breaking changes, codemods | `next-upgrade` / `vercel:next-upgrade` skill |
| Cache Components: `use cache`, cacheLife, cacheTag, updateTag, PPR | `next-cache-components` / `vercel:next-cache-components` skill |
| React 19 core APIs (useActionState, useFormStatus, useOptimistic, use(), ref as prop) | context7: `/vercel/next.js` + react.dev (query-docs) |
| RSC implementation and deployment checklist | `next-best-practices` skill |

---

## Best Practices Summary

### Component Boundaries
- Keep Client Components at the edges (leaves) of the component tree
- Use Server Components by default
- Extract minimal interactive parts to Client Components
- Pass Server Components as `children` to Client Components

### Data Fetching
- Fetch data in Server Components close to where it's used
- Use parallel fetching (`Promise.all`) for independent data
- Set appropriate cache and revalidate options
- Use `generateStaticParams` for static routes

### Performance
- Use Suspense boundaries for streaming
- Implement loading.tsx for instant loading states
- Enable PPR for static/dynamic mix
- Use route segment config to control rendering mode

---

## Templates

- **`scripts/server-component-template.tsx`** - Basic async Server Component with data fetching
- **`scripts/client-component-template.tsx`** - Interactive Client Component with hooks
- **`scripts/server-action-template.ts`** - Server Action with validation and revalidation
- **`scripts/create-server-component.md`** - Command-style scaffold; kept as the script-invocation contract exercised by `tests/skills/scripts/`

---

## Troubleshooting

| Error | Fix |
|-------|-----|
| "You're importing a component that needs useState" | Add `'use client'` directive |
| "async/await is not valid in non-async Server Components" | Add `async` to function declaration |
| "Cannot use Server Component inside Client Component" | Pass Server Component as `children` prop |
| "Hydration mismatch" | Use `'use client'` for Date.now(), Math.random(), browser APIs |
| "params is not defined" or params returning Promise | Add `await` before `params` (Next.js 16 breaking change) |
| "experimental_ppr is not a valid export" | Use Cache Components with `"use cache"` directive instead |
| "cookies/headers is not a function" | Add `await` before `cookies()` or `headers()` (Next.js 16) |

---

## Resources

- [Next.js 16 Documentation](https://nextjs.org/docs)
- [React 19.2 Blog Post](https://react.dev/blog/2025/10/01/react-19-2)
- [React Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md)
- [App Router Migration Guide](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration)

---

## Related Skills

After mastering React Server Components:
1. **Streaming API Patterns** - Real-time data patterns
2. **Type Safety & Validation** - tRPC integration
3. **Edge Computing Patterns** - Global deployment
4. **Performance Optimization** - Core Web Vitals

---

## Capability Details

Keyword and problem-mapping metadata for each RSC capability (react-19-patterns, use-hook-suspense, optimistic-updates-async, rsc-patterns, server-actions, data-fetching, streaming-ssr, caching, cache-components, tanstack-router-patterns, async-params, nextjs-16-upgrade).

Load full capability details: `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/react-server-components-framework/references/capability-details.md")`

---

## Rules (5)

### Scope RSC cache keys properly to prevent leaking user-specific data across requests — CRITICAL


## RSC: Cache Safety

The `"use cache"` directive in Next.js 16 Cache Components generates cache keys from arguments, closures, and build ID. If user-specific data is fetched inside a cached function without a user-distinguishing key, the first user's response is served to every subsequent user. Runtime APIs (`cookies()`, `headers()`) cannot be called directly inside `"use cache"` blocks.

**Incorrect — runtime API inside cache:**
```tsx
async function CachedDashboard() {
  'use cache'
  const token = cookies().get('token') // Error: cookies() cannot be used inside 'use cache'
  const data = await fetchUserData(token)
  return <Dashboard data={data} />
}
```

**Correct — read runtime values outside, pass as arguments:**
```tsx
async function DashboardPage() {
  const token = (await cookies()).get('token')?.value ?? ''
  return <CachedDashboard token={token} />
}

async function CachedDashboard({ token }: { token: string }) {
  'use cache'
  // token is now part of the cache key — each user gets their own entry
  const data = await fetchUserData(token)
  return <Dashboard data={data} />
}
```

**Incorrect — awaiting dynamic promises inside cache:**
```tsx
async function Cached({ promise }: { promise: Promise<unknown> }) {
  'use cache'
  const data = await promise // Causes build hang — promise is not serializable
  return <div>{data}</div>
}
```

**Correct — resolve outside, pass the value:**
```tsx
async function Parent() {
  const value = await getDynamicValue()
  return <Cached value={value} />
}

async function Cached({ value }: { value: string }) {
  'use cache'
  return <div>{value}</div>
}
```

**Key rules:**
- Never call `cookies()`, `headers()`, or read `searchParams` inside a `"use cache"` block — read them in a parent component and pass as serializable arguments.
- Every argument passed to a cached function becomes part of the cache key. Include user-identifying values (userId, token) to prevent cross-user data leaks.
- Do not `await` dynamic Promises inside `"use cache"` — resolve them outside and pass the result.
- Nested `"use cache"` functions have isolated scopes; `React.cache` values from an outer function are not visible in an inner one. Cache at the appropriate level and compose via `children`.
- Use `cacheTag()` with user-scoped tags (e.g., `user-$\{userId\}`) to enable targeted invalidation.

Reference: first-party `next-cache-components` / `vercel:next-cache-components` skill (constraints, pitfalls); house scars: `references/ork-delta.md`


### Minimize RSC client boundaries to avoid shipping unnecessary JavaScript to the browser — CRITICAL


## RSC: Client Boundaries

The `'use client'` directive marks the boundary between Server and Client component trees. Every component imported by a Client Component becomes a Client Component too. Push `'use client'` to the smallest interactive leaf components to keep the server-rendered surface area as large as possible.

**Incorrect:**
```tsx
// app/products/page.tsx
'use client' // Entire page is now a client component

import { useState, useEffect } from 'react'

export default function ProductsPage() {
  const [products, setProducts] = useState([])

  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts)
  }, [])

  return (
    <div>
      <h1>Products</h1>
      <ProductFilters />
      <ProductList products={products} />
    </div>
  )
}
```

**Correct:**
```tsx
// app/products/page.tsx — Server Component (default, no directive)
import { db } from '@/lib/database'
import { ProductFilters } from '@/components/ProductFilters'

export default async function ProductsPage() {
  const products = await db.product.findMany()

  return (
    <div>
      <h1>Products</h1>
      <ProductFilters />             {/* Client Component — leaf */}
      <ProductList products={products} /> {/* Server Component */}
    </div>
  )
}

// components/ProductFilters.tsx — only the interactive leaf is 'use client'
'use client'

import { useState } from 'react'

export function ProductFilters() {
  const [filter, setFilter] = useState('')
  return (
    <input
      value={filter}
      onChange={(e) => setFilter(e.target.value)}
      placeholder="Filter..."
    />
  )
}
```

**Key rules:**
- Never add `'use client'` to page or layout files; extract interactive parts into dedicated leaf components.
- Server Components can render Client Components, but Client Components **cannot** directly import Server Components — use the `children` prop pattern instead.
- Pass Server Components into Client Components via `children` or render-prop slots so they stay server-rendered.
- Every module imported by a `'use client'` file is pulled into the client bundle — keep imports minimal.

Reference: first-party `next-best-practices` / `vercel:nextjs` skill (RSC boundaries, composition)


### Use correct React 19 component types instead of deprecated React.FC patterns — MEDIUM


## RSC: Component Types

React 19 deprecates `React.FC`. It previously added implicit `children` to all component props, which caused incorrect type-checking. Use function declarations (preferred) or typed arrow functions with explicit `React.ReactNode` return types.

**Incorrect:**
```tsx
'use client'

import React from 'react'

// DEPRECATED: React.FC adds implicit children and is removed from React 19 best practices
export const Button: React.FC<ButtonProps> = ({ children, onClick }) => {
  return <button onClick={onClick}>{children}</button>
}

// Also problematic: no return type annotation
export const Card = ({ title, body }: CardProps) => {
  return (
    <div>
      <h2>{title}</h2>
      <p>{body}</p>
    </div>
  )
}
```

**Correct:**
```tsx
'use client'

// PREFERRED: Function declaration with explicit return type
export function Button({ children, onClick }: ButtonProps): React.ReactNode {
  return <button onClick={onClick}>{children}</button>
}

// ALSO VALID: Arrow function without React.FC, with explicit return type
export const Card = ({ title, body }: CardProps): React.ReactNode => {
  return (
    <div>
      <h2>{title}</h2>
      <p>{body}</p>
    </div>
  )
}
```

**React 19 ref handling:**
```tsx
'use client'

// React 19: ref is a regular prop — no forwardRef needed
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  ref?: React.Ref<HTMLInputElement>
}

export function Input({ ref, ...props }: InputProps): React.ReactNode {
  return <input ref={ref} {...props} />
}

// Usage
const inputRef = useRef<HTMLInputElement>(null)
<Input ref={inputRef} placeholder="Enter text..." />
```

**Key rules:**
- Use function declarations for components; they are hoisted and consistently identifiable in stack traces.
- Always annotate the return type as `React.ReactNode` for clarity and type safety.
- Do not use `React.FC` or `React.FunctionComponent` in React 19 projects.
- In React 19, pass `ref` as a regular prop — `forwardRef` is no longer required.

Reference: `references/ork-delta.md` (React.FC and forwardRef ban, house standard); react.dev React 19 upgrade guide


### Prevent RSC hydration mismatches that cause visual flicker and degraded performance — HIGH


## RSC: Hydration

Hydration attaches event listeners to server-rendered HTML. If the client render produces different output than the server render, React throws a hydration mismatch warning and falls back to client-side rendering. Common causes: accessing browser APIs during render, using non-deterministic values (`Date.now()`, `Math.random()`), and conditional rendering based on client-only state.

**Incorrect — non-deterministic value in render:**
```tsx
'use client'

function TimestampBadge() {
  // Server renders one value, client renders another → mismatch
  return <span>{Date.now()}</span>
}
```

**Correct — defer to useEffect:**
```tsx
'use client'

import { useState, useEffect } from 'react'

function TimestampBadge() {
  const [time, setTime] = useState<number | null>(null)

  useEffect(() => {
    setTime(Date.now())
  }, [])

  return <span>{time ?? 'Loading...'}</span>
}
```

**Incorrect — browser API access during render:**
```tsx
'use client'

function ScreenWidth() {
  // window is undefined on the server → crash or mismatch
  const width = window.innerWidth
  return <p>Width: {width}px</p>
}
```

**Correct — browser API in useEffect with state:**
```tsx
'use client'

import { useState, useEffect } from 'react'

function ScreenWidth() {
  const [width, setWidth] = useState(0)

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    handleResize()
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return <p>Width: {width}px</p>
}
```

**Key rules:**
- Never access `window`, `document`, `navigator`, `localStorage`, or other browser APIs during render — always use `useEffect`.
- Avoid non-deterministic expressions (`Date.now()`, `Math.random()`, `crypto.randomUUID()`) in JSX — initialize as `null` and set in `useEffect`.
- Use `suppressHydrationWarning` only for intentional, harmless mismatches — never to silence bugs.
- For components that depend entirely on browser APIs, use a `ClientOnly` wrapper (mount guard via `useEffect`) or `next/dynamic` with `ssr: false`.

Reference: first-party `next-best-practices` / `vercel:nextjs` skill (hydration); react.dev hydration-mismatch docs


### Pass only serializable props across the RSC server-client boundary to avoid runtime errors — CRITICAL


## RSC: Serialization

Only serializable data can cross the Server-to-Client Component boundary. React must serialize props into the RSC payload sent over the wire. Passing non-serializable values causes build or runtime errors.

**Incorrect:**
```tsx
// app/dashboard/page.tsx — Server Component
export default async function Dashboard() {
  const data = await getData()
  return (
    <ClientCard
      item={data}
      onClick={() => console.log('clicked')} // Functions cannot be serialized
      formatter={new Intl.NumberFormat('en-US')} // Class instances cannot be serialized
      icon={Symbol('star')} // Symbols cannot be serialized
    />
  )
}
```

**Correct:**
```tsx
// app/dashboard/page.tsx — Server Component
import { handleClick } from '@/app/actions' // Server Action

export default async function Dashboard() {
  const data = await getData()
  return (
    <ClientCard
      item={{ id: data.id, name: data.name, price: data.price }} // Plain object
      tags={['featured', 'sale']}  // Array of primitives
      isActive={true}              // Boolean
      onAction={handleClick}       // Server Actions ARE serializable
    />
  )
}

// components/ClientCard.tsx
'use client'

export function ClientCard({ item, tags, isActive, onAction }: ClientCardProps) {
  const handleLocalClick = () => onAction(item.id) // Call server action

  return (
    <div onClick={handleLocalClick}>
      <h2>{item.name}</h2>
      <p>{isActive ? 'Active' : 'Inactive'}</p>
    </div>
  )
}
```

**Serializable types** (safe to pass as props):
- Primitives: `string`, `number`, `bigint`, `boolean`, `null`, `undefined`
- Plain objects and arrays containing serializable values
- Server Actions (functions defined with `'use server'`)
- `Date`, `Map`, `Set`, `TypedArray`, `ArrayBuffer`

**Non-serializable types** (will error at the boundary):
- Regular functions and closures
- Class instances (`new Intl.NumberFormat()`, `new URL()`, custom classes)
- Symbols, `WeakMap`, `WeakSet`

**Key rules:**
- Define event handlers (`onClick`, `onChange`) inside the Client Component, not in the Server Component.
- Use Server Actions (`'use server'`) when you need to pass callable behavior from server to client.
- Convert class instances to plain objects before passing: `\{ url: myUrl.toString() \}` instead of `myUrl`.
- When in doubt, check if `JSON.stringify(prop)` would succeed — that is a reasonable (though not exact) heuristic.

Reference: first-party `next-best-practices` / `vercel:nextjs` skill (RSC boundaries, serializable props)



---

## References (3)

### Capability Details

# Capability Details

Keyword and problem-mapping metadata for each RSC capability.

## react-19-patterns
**Keywords:** react 19, React.FC, forwardRef, useActionState, useFormStatus, useOptimistic, function declaration
**Solves:**
- How do I replace React.FC in React 19?
- forwardRef replacement pattern
- useActionState vs useFormState
- React 19 component declaration best practices

## use-hook-suspense
**Keywords:** use(), use hook, suspense, promise, data fetching, promise cache, cachePromise
**Solves:**
- How do I use the use() hook in React 19?
- Suspense-native data fetching pattern
- Promise caching to prevent infinite loops

## optimistic-updates-async
**Keywords:** useOptimistic, useTransition, optimistic update, instant ui, auto rollback
**Solves:**
- How to show instant UI updates before API responds?
- useOptimistic with useTransition pattern
- Auto-rollback on API failure

## rsc-patterns
**Keywords:** rsc, server component, client component, use client, use server
**Solves:**
- When to use server vs client components?
- RSC boundaries and patterns

## server-actions
**Keywords:** server action, form action, use server, mutation
**Solves:**
- How do I create a server action?
- Form handling with server actions

## data-fetching
**Keywords:** fetch, data fetching, async component, loading, suspense
**Solves:**
- How do I fetch data in RSC?
- Async server components

## streaming-ssr
**Keywords:** streaming, ssr, suspense boundary, loading ui
**Solves:**
- How do I stream server content?
- Progressive loading patterns

## caching
**Keywords:** cache, revalidate, static, dynamic, isr
**Solves:**
- How do I cache in Next.js 16?
- Revalidation strategies

## cache-components
**Keywords:** use cache, cacheLife, cacheTag, cacheComponents, revalidateTag, updateTag, cache directive
**Solves:**
- How do I use the "use cache" directive?
- What replaced experimental_ppr?
- How do I set cache duration with cacheLife?
- How do I invalidate cache with cacheTag?
- How do I migrate from Next.js 15 fetch caching to use cache?

## tanstack-router-patterns
**Keywords:** tanstack router, react router, vite, spa, client rendering, prefetch
**Solves:**
- How do I use React 19 features without Next.js?
- TanStack Router prefetching setup
- Route-based data fetching with TanStack Query

## async-params
**Keywords:** async params, searchParams, Promise params, await params, dynamic route params
**Solves:**
- How do I access params in Next.js 16?
- Why are my route params undefined?
- How do I use searchParams in Next.js 16?
- How do I type params as Promise?

## nextjs-16-upgrade
**Keywords:** next.js 16, nextjs 16, upgrade, migration, breaking changes, async params, turbopack, proxy.ts, cache components
**Solves:**
- How do I upgrade to Next.js 16?
- What are the breaking changes in Next.js 16?
- How do I migrate middleware.ts to proxy.ts?
- How do I use async params and searchParams?
- What replaced experimental_ppr?
- How do I use the new caching APIs?


### Ork Delta

# ork delta: react-server-components-framework

House rules and scars only. Vendor tutorials for every topic this skill wraps live
in first-party sources; see "Upstream coverage (do not restate)" in SKILL.md.
Each entry below exists because OrchestKit shipped a wrong or house-specific
version of it first.

## Pass a cacheLife profile as the second argument to revalidateTag
Why: PR #2143 (2026-05-31 library-currency audit, next-react cluster) caught this skill teaching the fabricated one-argument form. In Next.js 16 the real signature is `revalidateTag(tag, profile)` (for example `'max'` or `\{ expire: 3600 \}`); the one-argument call is deprecated and a TypeScript error, there is no array overload (loop one call per tag), and read-your-writes invalidation uses `updateTag(tag)` instead.
Upstream: skill next-cache-components / vercel:next-cache-components (marketplace)

## Import proxy.ts types from next/server, never from a next/proxy module
Why: PR #2143 removed a fully fabricated `next/proxy` API (ProxyRequest, ProxyResponse, `redirect()`, `next()` helpers) that this skill had documented. The real Next.js 16 change is only: rename `middleware.ts` to `proxy.ts`, export a named `proxy` function, keep `NextRequest`/`NextResponse` from `next/server`, same function body; and `proxy.ts` runs on the Node.js runtime only (no Edge).
Upstream: skill next-upgrade / vercel:next-upgrade (marketplace)

## Write 'use cache' as a directive, not a cache() wrapper from next/cache
Why: PR #2143 removed a fabricated `import \{ cache \} from 'next/cache'` wrapper this skill had documented. The real API is the `'use cache'` directive (plus `'use cache: remote'` and `'use cache: private'` in 16.2) with the `cacheLife()` / `cacheTag()` helpers, whose `unstable_` prefix was dropped in 16.2; the `experimental_ppr` flag is replaced by `cacheComponents: true` in next.config.ts.
Upstream: skill next-cache-components / vercel:next-cache-components (marketplace)

## Format Server Action Zod errors with z.treeifyError, not .flatten
Why: PR #2143 (frontend-libs cluster, Zod 4 currency fix): `.flatten()` is deprecated in Zod 4 and the replacement changes the payload shape, per-field messages move to `tree.properties[field].errors`, which silently breaks clients still reading `fieldErrors`.
Upstream: Zod 4 error formatting docs (https://zod.dev)

## Declare React components as function declarations, never React.FC or forwardRef
Why: House standard adopted with the React 19 floor bump (distilled 2026-07-31 from the retired references/react-19-patterns.md; no traced incident). React 19 drops React.FC's implicit `children` and makes `ref` a regular prop, so the house standard is function declarations with explicit `children` / `ref` props, an explicit `React.ReactNode` return type, and an ESLint ban-types rule that blocks `React.FC` and `React.FunctionComponent`.
Upstream: react.dev React 19 upgrade guide (context7: /vercel/next.js + react.dev query-docs)

## Cache the promise before handing it to the use() hook
Why: Standard React 19 trap kept as a house rule (distilled from the retired react-19-patterns.md; no traced incident): a promise created during render makes `use()` re-suspend on every pass, an infinite loop. House pattern is a keyed promise cache (`cachePromise(key, fetcher)`) that deletes rejected entries so retry works, with explicit invalidation on mutation and a full clear on logout.
Upstream: react.dev use() reference (context7 query-docs)

## Use use() only for one-shot reads, TanStack Query for everything else
Why: House rule of thumb (adopted with the 2026-07-31 wrap-plus-delta campaign): `use()` covers read-only display of a single fetch; mutations, refetching, background refresh, optimistic updates, and infinite scroll stay on TanStack Query. Do not rebuild query-cache management around `use()`.
Upstream: TanStack Query docs (https://tanstack.com/query/latest) and react.dev use() reference


### Tanstack Router Patterns

# React 19 + TanStack Router Patterns

> **OrchestKit Supplement** (Dec 2025) - Patterns for React 19 applications using TanStack Router instead of Next.js App Router.

## Overview

While the main skill covers Next.js 16 + React Server Components, **OrchestKit uses React 19 with TanStack Router**. This supplement documents the equivalent patterns for client-rendered SPAs with React 19's new features.

## Key Differences from Next.js RSC

| Pattern | Next.js 16 App Router | React 19 + TanStack Router |
|---------|----------------------|---------------------------|
| Data Fetching | Server Components | TanStack Query + route loaders |
| Mutations | Server Actions | React 19 `useActionState` + API calls |
| Optimistic UI | Experimental `useOptimistic` | React 19 `useOptimistic` (stable) |
| Transitions | `useTransition` | Same - `useTransition` |
| Promise Handling | `use()` in Server Components | `use()` in Client Components |
| Prefetching | Route segment prefetching | TanStack Router `defaultPreload: 'intent'` |

---

## Pattern 1: Route-Based Data Fetching

### TanStack Router with Query Integration

```tsx
// router.tsx
import { createRouter, createRootRoute, createRoute } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()

const rootRoute = createRootRoute({
  component: RootLayout,
})

const analysisRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: 'analyze/$analysisId',
  // ★ Prefetch with intent-based preloading
  loader: ({ params }) => {
    queryClient.prefetchQuery({
      queryKey: ['analysis', params.analysisId],
      queryFn: () => fetchAnalysis(params.analysisId),
      staleTime: 5 * 60 * 1000, // 5 minutes
    })
  },
  component: AnalysisPage,
})

export const router = createRouter({
  routeTree: rootRoute.addChildren([analysisRoute]),
  defaultPreload: 'intent',  // Preload on hover
  defaultPreloadDelay: 50,   // 50ms delay before preload
  defaultStaleTime: 5 * 60 * 1000, // 5 minutes
})
```

---

## Pattern 2: React 19 useOptimistic

### Optimistic Updates Without Server Actions

```tsx
import { useOptimistic, useTransition, useState } from 'react'

interface AnalysisCard {
  id: string
  title: string
  status: 'pending' | 'analyzing' | 'complete'
}

export function AnalysisList({ analyses }: { analyses: AnalysisCard[] }) {
  const [optimisticAnalyses, addOptimistic] = useOptimistic(
    analyses,
    (current, newAnalysis: AnalysisCard) => [...current, newAnalysis]
  )
  const [isPending, startTransition] = useTransition()

  async function handleSubmit(url: string) {
    // Create optimistic placeholder
    const optimistic: AnalysisCard = {
      id: `temp-${Date.now()}`,
      title: url,
      status: 'pending',
    }

    startTransition(async () => {
      addOptimistic(optimistic)  // Show immediately

      const result = await createAnalysis({ url })  // Real API call
      // React reconciles automatically when analyses prop updates
    })
  }

  return (
    <div>
      {optimisticAnalyses.map(analysis => (
        <Card key={analysis.id} analysis={analysis} />
      ))}
    </div>
  )
}
```

---

## Pattern 3: useActionState for Form Handling

### React 19 Form Actions (Without Server Actions)

```tsx
import { useActionState, use } from 'react'
import { z } from 'zod'

const UrlSchema = z.object({
  url: z.url('Please enter a valid URL'),
})

async function submitUrl(
  prevState: { error: string | null; success: boolean },
  formData: FormData
) {
  const result = UrlSchema.safeParse({ url: formData.get('url') })

  if (!result.success) {
    return { error: result.error.errors[0].message, success: false }
  }

  try {
    await api.post('/api/v1/analyses', { url: result.data.url })
    return { error: null, success: true }
  } catch (error) {
    return { error: 'Failed to start analysis', success: false }
  }
}

export function UrlInputForm() {
  const [state, formAction, isPending] = useActionState(submitUrl, {
    error: null,
    success: false,
  })

  return (
    <form action={formAction}>
      <input
        type="url"
        name="url"
        placeholder="https://example.com/article"
        disabled={isPending}
      />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Analyzing...' : 'Analyze'}
      </button>
      {state.error && <p className="error">{state.error}</p>}
    </form>
  )
}
```

---

## Pattern 4: use() Hook for Promise Handling

### Suspense-Based Data Fetching in Client Components

```tsx
import { use, Suspense } from 'react'

// Cache the promise at module level or use a query cache
const analysisPromise = fetchAnalysis(analysisId)

function AnalysisDetails({ analysisId }: { analysisId: string }) {
  // ★ use() unwraps promises in render, works with Suspense
  const analysis = use(analysisPromise)

  return (
    <div>
      <h1>{analysis.title}</h1>
      <p>Status: {analysis.status}</p>
    </div>
  )
}

// Usage with Suspense boundary
function AnalysisPage() {
  return (
    <Suspense fallback={<AnalysisSkeleton />}>
      <AnalysisDetails analysisId="123" />
    </Suspense>
  )
}
```

### With TanStack Query (Recommended)

```tsx
import { useSuspenseQuery } from '@tanstack/react-query'

function AnalysisDetails({ analysisId }: { analysisId: string }) {
  // useSuspenseQuery integrates with React 19's Suspense
  const { data: analysis } = useSuspenseQuery({
    queryKey: ['analysis', analysisId],
    queryFn: () => fetchAnalysis(analysisId),
  })

  return <div>{analysis.title}</div>
}
```

---

## Pattern 5: Prefetching Strategy

### Intent-Based Preloading

```tsx
// hooks/usePrefetch.ts
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
import { useCallback } from 'react'

export function usePrefetch() {
  const queryClient = useQueryClient()
  const router = useRouter()

  const prefetchAnalysis = useCallback((analysisId: string) => {
    // Prefetch route data
    router.preloadRoute({
      to: '/analyze/$analysisId',
      params: { analysisId },
    })

    // Prefetch query data
    queryClient.prefetchQuery({
      queryKey: ['analysis', analysisId],
      queryFn: () => fetchAnalysis(analysisId),
      staleTime: 5 * 60 * 1000,
    })
  }, [queryClient, router])

  return { prefetchAnalysis }
}

// Usage in component
function SkillCard({ skill }) {
  const { prefetchAnalysis } = usePrefetch()

  return (
    <Link
      to="/analyze/$analysisId"
      params={{ analysisId: skill.id }}
      onMouseEnter={() => prefetchAnalysis(skill.id)}
    >
      {skill.title}
    </Link>
  )
}
```

---

## Pattern 6: Exhaustive Type Checking

### assertNever for Type-Safe Switch Statements

```tsx
// lib/utils.ts
export function assertNever(value: never, message?: string): never {
  throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`)
}

// Usage in component
type AnalysisStatus = 'pending' | 'analyzing' | 'complete' | 'failed'

function StatusBadge({ status }: { status: AnalysisStatus }) {
  switch (status) {
    case 'pending':
      return <Badge variant="secondary">Pending</Badge>
    case 'analyzing':
      return <Badge variant="info">Analyzing</Badge>
    case 'complete':
      return <Badge variant="success">Complete</Badge>
    case 'failed':
      return <Badge variant="destructive">Failed</Badge>
    default:
      // TypeScript error if new status added but not handled
      return assertNever(status, `Unhandled status: ${status}`)
  }
}
```

---

## OrchestKit-Specific Patterns

### 1. SSE Event Handling with Zustand

```tsx
// stores/sseStore.ts
import { create } from 'zustand'

interface SSEEvent {
  event_id: string
  type: string
  data: unknown
}

interface SSEStore {
  events: Map<string, SSEEvent>  // O(1) deduplication
  addEvent: (event: SSEEvent) => void
}

export const useSSEStore = create<SSEStore>((set) => ({
  events: new Map(),
  addEvent: (event) => set((state) => {
    // O(1) lookup for deduplication
    if (state.events.has(event.event_id)) {
      return state  // Already processed
    }
    const newEvents = new Map(state.events)
    newEvents.set(event.event_id, event)
    return { events: newEvents }
  }),
}))
```

### 2. List Virtualization

```tsx
// components/VirtualizedGrid.tsx
import { useVirtualizer } from '@tanstack/react-virtual'

export function VirtualizedGrid<T>({ items, renderItem }: Props<T>) {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,  // Estimated row height
    overscan: 5,  // Render 5 extra items above/below viewport
  })

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div
        style={{
          height: virtualizer.getTotalSize(),
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: virtualItem.start,
              width: '100%',
            }}
          >
            {renderItem(items[virtualItem.index])}
          </div>
        ))}
      </div>
    </div>
  )
}
```

---

## Migration Checklist

When migrating Next.js patterns to TanStack Router:

- [ ] Replace `use server` with client-side API calls + `useActionState`
- [ ] Replace `generateStaticParams` with route loader prefetching
- [ ] Replace `revalidatePath` with TanStack Query `invalidateQueries`
- [ ] Replace Next.js `Image` with native `<img>` + loading="lazy"
- [ ] Replace `cookies()`/`headers()` with browser APIs or API calls
- [ ] Replace `Metadata` exports with `document.title` or react-helmet

---

## References

- [React 19 Release Notes](https://react.dev/blog/2024/12/05/react-19)
- [TanStack Router Docs](https://tanstack.com/router/latest)
- [TanStack Query with Suspense](https://tanstack.com/query/latest/docs/framework/react/guides/suspense)
- [Zod Validation](https://zod.dev/)
