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.

  • Frontend
  • $react-server-components
  • 210 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- react-server-components

Installs to ~/.agents/skills/react-server-components/ for every project on this machine.

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

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.

~/.agents/skills/react-server-components/SKILL.md

--- name: react-server-components description: Designs React Server Component boundaries — where "use client" goes, what props can cross the boundary, how to fetch data on the server without… license: MIT ---

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 children or other JSX props. That is how server content nests inside client shells.

Workflow

  1. Read AGENTS.md and the framework config. Confirm which RSC framework is in use (Next.js App Router, React Router RSC mode, Waku, Parcel) and the React version.
  2. 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.
  3. Decide each component’s side with the table below. Default to server.
  4. Move the boundary down: split the interactive bit into its own client file and keep its parent on the server.
  5. Fix data flow: fetch in the Server Component that renders the data, start independent requests together, and stream the slow ones.
  6. Guard secrets with import "server-only" in every module that touches credentials, the database, or private env vars.
  7. Verify with the checks at the end, including the client bundle.

Server or client

The component needs…Side
useState, useReducer, effects, refs to DOMclient
Event handlers (onClick, onChange)client
Browser APIs (window, localStorage, IntersectionObserver)client
Context consumers (useContext, theme, i18n hooks)client
Database, filesystem, secrets, private API keysserver
Large render-only libraries (markdown, syntax highlighting, date formatting)server
Static or per-request markup with no interactionserver
Form submission that mutates dataserver 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 ComponentNot allowed
string, number, boolean, null, undefined, bigintfunctions (except server functions)
plain objects and arrays of allowed valuesclass instances (ORM models, Decimal, custom errors)
Date, Map, Set, typed arrays, ArrayBuffer, FormDatasymbols not created with Symbol.for
Promises of allowed values (read with use())DOM nodes, streams of non-serializable values
JSX, including Server Componentsobjects 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.cache so 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.all when 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 shared components/index.ts barrel 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.
  • Date crosses fine, but timezone formatting on server and client differs and causes hydration mismatches; format on one side only.
  • Libraries that read window at 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem