React Server Components skill for Codex
Put the client boundary at the smallest leaf. Codex decides server versus client per component from a needs table, splits mixed components so only the interactive part ships JavaScript, and fixes the 'functions cannot be passed' error at its cause. It maps ORM rows to plain DTOs, dedupes loaders with React.cache, removes await waterfalls, and adds server-only guards so credentials never reach the browser payload.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- react-server-components Installs to ~/.agents/skills/react-server-components/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install react-server-components The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project react-server-components Installs to .agents/skills/react-server-components/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/react-server-components.zipunzip -o react-server-components.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) react-server-components PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $react-server-components, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs React Server Component boundaries — where "use client" goes, what props can cross the boundary, how to fetch data on the server without waterfalls, how to stream with Suspense, and how to keep secrets and server-only code out of client bundles. Use when a component errors with "functions cannot be passed to Client Components", when deciding server vs client for a component, when a client bundle grows after adding RSC, or when structuring data fetching in Next.js App Router or another RSC framework.
Use it when
- Deciding whether a component should be a server or client component
- A serialization error appears at a client boundary
- Client bundle size jumped after moving to an RSC framework
- Structuring server data fetching and streaming for a route
Use something else when
- The question is cache lifetimes and revalidation in Next.js: use nextjs-caching
- The task is writing or securing mutations: use nextjs-server-actions
How to check the result
The build passes, the route's client JS is no larger than before, and the RSC payload in the Network tab contains no secrets or unused fields.
The mistake it prevents
Adding "use client" to a layout or barrel file, which silently pulls an entire subtree and its heavy imports into the browser bundle.
Done well means: the client boundary sits at the smallest interactive leaf, only serializable data crosses it, data is fetched in parallel on the server close to where it is rendered, slow parts stream behind Suspense, and nothing marked server-only can end up in a browser bundle.
Mental model in five lines
- A component is a Server Component unless its module has
"use client"or is imported, directly or transitively, by a module that does. "use client"marks an entry point into the client graph. Everything that file imports becomes client code too.- Server Components run once per request (or at build time), never re-render on the client, and cannot use state, effects, or browser APIs.
- Props passed from server to client are serialized into the RSC payload. Treat that payload as public.
- A Server Component can pass other Server Components to a Client Component as
childrenor other JSX props. That is how server content nests inside client shells.
Workflow
- Read
AGENTS.mdand the framework config. Confirm which RSC framework is in use (Next.js App Router, React Router RSC mode, Waku, Parcel) and the React version. - Map the tree for the route you are changing: which files have
"use client", which fetch data, which import heavy libraries.grep -rln '"use client"' app src. - Decide each component’s side with the table below. Default to server.
- Move the boundary down: split the interactive bit into its own client file and keep its parent on the server.
- Fix data flow: fetch in the Server Component that renders the data, start independent requests together, and stream the slow ones.
- Guard secrets with
import "server-only"in every module that touches credentials, the database, or private env vars. - Verify with the checks at the end, including the client bundle.
Server or client
| The component needs… | Side |
|---|---|
useState, useReducer, effects, refs to DOM | client |
Event handlers (onClick, onChange) | client |
Browser APIs (window, localStorage, IntersectionObserver) | client |
Context consumers (useContext, theme, i18n hooks) | client |
| Database, filesystem, secrets, private API keys | server |
| Large render-only libraries (markdown, syntax highlighting, date formatting) | server |
| Static or per-request markup with no interaction | server |
| Form submission that mutates data | server function called from either side |
If a component needs both, split it: server parent fetches and renders the static part, client child receives plain data and handles interaction.
// app/products/[id]/page.tsx (server)
import { AddToCart } from "./add-to-cart";
import { getProduct } from "@/lib/products";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
return (
<article>
<h1>{product.name}</h1>
<Markdown source={product.description} /> {/* stays on the server */}
<AddToCart productId={product.id} price={product.priceCents} />
</article>
);
}
// app/products/[id]/add-to-cart.tsx
"use client";
import { useState } from "react";
export function AddToCart({ productId, price }: { productId: string; price: number }) {
const [qty, setQty] = useState(1);
// ...
}
What can cross the boundary
| Allowed as props to a Client Component | Not allowed |
|---|---|
| string, number, boolean, null, undefined, bigint | functions (except server functions) |
| plain objects and arrays of allowed values | class instances (ORM models, Decimal, custom errors) |
Date, Map, Set, typed arrays, ArrayBuffer, FormData | symbols not created with Symbol.for |
Promises of allowed values (read with use()) | DOM nodes, streams of non-serializable values |
| JSX, including Server Components | objects with prototype methods you rely on |
Server functions ("use server" exports) |
Fixes for the common error “Functions cannot be passed directly to Client Components”:
- An event handler defined in a server file: move the handler into the client component.
- A callback that must run server logic: make it a server function in a
"use server"module and pass that. - A formatter or render prop: render the result on the server and pass JSX or a string.
ORM rows often carry class prototypes or Decimal values. Map them to plain DTOs at the
data-access layer, picking only the fields the client needs; this also stops private
columns from leaking into the payload.
Data fetching
- Fetch where the data is rendered; do not fetch at the top and prop-drill.
- Deduplicate per request with
React.cacheso several components can call the same loader cheaply:
// lib/users.ts
import "server-only";
import { cache } from "react";
export const getUser = cache(async (id: string) => {
const row = await db.user.findUnique({ where: { id } });
return row && { id: row.id, name: row.name, avatarUrl: row.avatarUrl };
});
- Start independent requests before awaiting any of them:
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);
- Stream slow, non-critical sections. Pass the promise down and let a client child
use()it, or make the slow part its own async Server Component inside<Suspense>:
export default async function Dashboard() {
const summary = await getSummary(); // fast, needed for the shell
return (
<>
<Summary data={summary} />
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* async server component, awaits its own data */}
</Suspense>
</>
);
}
- Awaiting sequentially in nested components creates a waterfall: parent awaits, then
renders a child that awaits. Hoist the promise creation or use
Promise.allwhen the child’s input does not depend on the parent’s result.
Caching across requests is framework specific. For Next.js, use nextjs-caching.
Context and providers
Context providers must be client components. Wrap only what needs them and keep the provider file small:
// app/providers.tsx
"use client";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}
children stays server-rendered even though Providers is a client component.
Security rules
import "server-only"at the top of any module with secrets or DB access. A client import then fails the build instead of shipping the code.- Only env vars with the framework’s public prefix (
NEXT_PUBLIC_,VITE_) are inlined into client code. Never rename a secret to add the prefix to fix an undefined value. - Every prop to a client component is visible in page source. Never pass a full user record, tokens, or internal IDs that the UI does not need.
- Server functions are public HTTP endpoints. Authenticate and authorize inside each
one; for Next.js specifics use
nextjs-server-actions.
Gotchas
- Putting
"use client"in a layout or a sharedcomponents/index.tsbarrel converts a large subtree to client code. Check where the directive lives before adding imports. - A client component cannot import a Server Component. It can only receive one as a prop. Importing turns the server file into client code or fails on server-only imports.
- Third-party components that use hooks but lack
"use client"fail when rendered from a server file. Re-export them from a small client wrapper file. Datecrosses fine, but timezone formatting on server and client differs and causes hydration mismatches; format on one side only.- Libraries that read
windowat import time break server rendering even inside a client component. Load them with a dynamic import in an effect or the framework’s client-only dynamic import.
Verification
-
next build(or the framework build) passes; no “server-only” or serialization errors. - The route’s client JS did not grow unexpectedly: compare the build output sizes or run the bundle analyzer before and after.
- View source or the RSC payload in the Network tab: no secrets, tokens, or unused fields in serialized props.
- Slow sections show their fallback and stream in; the shell renders without waiting for them (throttle network or add an artificial delay locally).
- No hydration warnings in the browser console.
Output
Summarize as a short table: component, side (server/client), why, and any data now fetched in parallel or streamed. Call out boundary moves that changed bundle size.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "React Server Components"
short_description: "Put the client boundary at the smallest leaf"
default_prompt: "$react-server-components Why does this page ship 300 KB of JS when only the cart button is interactive?" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.