Claude Fable 5.1 & GPT-6 Astra packages are live

Client Components

Free

Client components — when interactivity justifies the bundle, hydration correctness, browser-API access, and keeping the boundary small.

184 lines7.4 KB Open Ai Frontend
targetModels
GPT-6 AstraGPT-5.6GPT-5.5GPT-5 FamilyFuture GPT Models
name
client-components
category
Frontend
description
Client components — when interactivity justifies the bundle, hydration correctness, browser-API access, and keeping the boundary small.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for ChatGPT: scripts/model-profiles.json -->

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names. Reading elsewhere is allowed; writing outside it is not, and a needed out-of-scope change is reported, not made. SIGNATURE_PINNING: Before implementing, write the exact signatures you will add or change (name, parameters, return type). Implement to those signatures; if one must change, say so before changing it. TYPE_CONTRACTS: Every public function carries explicit parameter and return types. No any, untyped dict, or interface{} at a module boundary.


#Purpose

Rules for "use client" components. Every one of them costs download, parse and execution time on the user's device, so each should exist for a reason that can be named: state, an event handler, a browser API, or an effect.

The server side is Frontend/server-components; hydration specifics are Frontend/hydration.


#"use client" is an entry point, not a file marker

tsx
"use client";
import { Chart } from "heavy-charting-lib";     // 180 KB — now in the bundle
import { formatMoney } from "@/lib/format";     // and this, and its imports

The directive marks the start of the client boundary. Everything reachable from it — transitively — is bundled and shipped.

Consequences worth remembering:

  • One "use client" at the top of a layout ships that entire subtree.
  • A utility imported by both server and client code ends up in the bundle.
  • Marking a file that needs no interactivity costs bytes for nothing.

Keep the boundary at the leaf:

tsx
// The page stays on the server; only the button is a client component
export default async function Page() {
  const product = await getProduct(id);
  return <article><ProductDetails product={product} /><AddToCart id={product.id} /></article>;
}

#Justify each one

A client component needs at least one of:

ReasonExample
State that changes from interactionA dropdown, a form field
An event handleronClick, onChange, onSubmit
A browser APIlocalStorage, matchMedia, IntersectionObserver
An effect synchronising with something externalAn analytics SDK, a map library
A third-party library that uses any of the aboveMost UI kits

If none applies, it is a server component.

Split rather than escalating: a card whose only interactive element is a "copy" button should be a server component containing a small client <CopyButton>, not a client component containing a card.


#Hydration must match

React renders on the server and then attaches on the client. If the two produce different HTML, React discards the server markup and re-renders — losing the performance benefit and, in some cases, producing visibly wrong content.

tsx
// Mismatch: the server and client evaluate these at different moments
<span>{new Date().toLocaleTimeString()}</span>
<span>{Math.random()}</span>
<span>{window.innerWidth}</span>        // window does not exist on the server

Correct patterns:

tsx
// Values that genuinely differ: render after mount
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <Placeholder />;

// Reading an external store: the server snapshot must be deterministic
const theme = useSyncExternalStore(subscribe, getClientSnapshot, () => "light");

Never read window, document, localStorage or navigator during render. They are undefined on the server. Read them in an effect or through useSyncExternalStore.

Use useId() for generated ids that must be stable across the boundary — a random id will differ between server and client. → Frontend/hydration


#Props and data

  • Props from a server component are serialised into the HTML. Pass the minimum, and never pass anything the user should not see.
  • Functions cannot cross the boundary, except Server Action references.
  • Server data belongs in a server component or a query cache, not fetched again on mount. Refetching on the client duplicates work and creates a waterfall. → Frontend/state-management
  • A client component receiving children from a server component does not bundle those children — that is the composition escape hatch for heavy content.

#Keep the bundle honest

  • Lazy-load heavy interactive components so they are not in the initial payload:
tsx
const Editor = dynamic(() => import("./Editor"), { ssr: false, loading: () => <Skeleton /> });
  • ssr: false for anything that genuinely cannot render on the server (a map, a canvas visualisation) — but it also means nothing renders until the JavaScript arrives, so reserve it.
  • Check what a "use client" file actually pulls in with a bundle analyser. A single icon import can drag in an entire library.
  • Prefer platform APIs over dependencies inside client components — every byte is paid by the user, on their device. → Frontend/performance

#Anti-patterns

Anti-patternWhy it failsFix
"use client" on a layout or pageThe whole subtree ships to the browserMark interactive leaves
Marking a file that needs no interactivityBytes for nothingLeave it on the server
A client component wrapping static contentShips content that could be HTMLInvert: server wraps client
Reading window during renderUndefined on the server; hydration errorEffect or useSyncExternalStore
Date/Math.random in renderServer and client differ; mismatchRender after mount
Random or index-derived idsDiffer across the boundaryuseId()
Suppressing hydration warningsHides a real mismatchFix the cause
Refetching server data on mountDuplicate work; waterfallPass it down or use a cache
Passing whole records as propsSerialised into the HTMLProject explicit fields
Heavy component in the initial bundleSlow time-to-interactivedynamic import
ssr: false by defaultNothing renders until JS loadsOnly where genuinely required
Not checking what a client file importsOne icon drags in a libraryBundle analysis

#Checklist

  • Every "use client" file has a named reason: state, handler, browser API or effect
  • The directive sits at interactive leaves, never at layouts or pages
  • Static content is not wrapped inside client components
  • No browser global is read during render
  • Time, randomness and environment-dependent values render after mount
  • Generated ids use useId()
  • No hydration warning is suppressed without fixing the cause
  • Server data is passed down or cached, not refetched on mount
  • Props crossing the boundary are minimal and explicitly projected
  • Heavy interactive components are dynamically imported
  • ssr: false is used only where server rendering is genuinely impossible
  • The client bundle has been analysed for unexpected dependencies