shadcn ui : RSC vs Client Boundaries
This skill is the rulebook for placing "use client" directives in a
Next.js App Router project that consumes shadcn primitives. It covers
which primitives are RSC-safe, which require the client boundary, where
the directive belongs in the file tree, how the components.json rsc
flag affects what the CLI emits, and how to build a page as a server
shell that hosts small client islands.
Companion skills :
shadcn-core-cli:components.jsonschema including therscflag.shadcn-impl-component-install: the install workflow that emits files with or without the directive.shadcn-syntax-form: the Form primitive (always client) and how it composes inside a server-rendered page.shadcn-syntax-dialog: the Dialog primitive (always client) and theTrigger+Contentboundary patterns.shadcn-agents-rsc-boundary-validator(B14) : the automated validator that lints a project's"use client"placement against this matrix.
Quick Reference : The Boundary Rule
The "use client" directive marks the BOUNDARY, not the dependency.
Everything imported below a "use client" file is bundled for the client.
Everything above is server-only.
ALWAYS draw the boundary as DEEP in the tree as possible. NEVER mark a
layout.tsx or page.tsx "use client" if a smaller leaf can carry
the directive instead. The whole point of App Router is to ship less
JavaScript ; a top-level "use client" defeats that.
ALWAYS : page.tsx (server) -> ClientIsland.tsx ("use client") -> Dialog
NEVER : page.tsx ("use client") -> Dialog
Quick Reference : Per-Primitive Compatibility Matrix
The full 60-row matrix lives in references/methods.md. Top-line summary :
RSC-SAFE (no "use client" required, pure presentation) :
Alert, AlertDialog content body, AspectRatio, Avatar, Badge, Breadcrumb,
Button, Card, Empty, Field, Input, InputGroup, Item, Kbd, Label, Pagination,
Separator, Skeleton, Spinner, Table primitives (Table/TableRow/...),
Textarea, Typography elements.
CLIENT-REQUIRED ("use client" mandatory, uses Radix state / hooks / browser API) :
Accordion, AlertDialog (root + trigger), Calendar, Carousel, Checkbox,
Collapsible, Combobox, Command, ContextMenu, Dialog, Drawer,
DropdownMenu, Form (react-hook-form), HoverCard, Input OTP, Menubar,
Native Select (interaction states), NavigationMenu, Popover, Progress
(animated variant), RadioGroup, Resizable, ScrollArea, Select, Sheet,
Sidebar (uses useSidebar), Slider, Sonner (Toaster mount), Switch, Tabs,
Toggle, ToggleGroup, Tooltip.
Rule of thumb : if the primitive wraps a Radix package, it carries
"use client". Static-HTML primitives (Card, Badge, Table, ...) do not.
Quick Reference : components.json rsc Flag
{
"$schema": "https://ui.shadcn.com/schema.json",
"rsc": true,
"tsx": true,
"style": "new-york",
"tailwind": { ... },
"aliases": { ... }
}
rsc: true -> CLI prepends "use client" to generated files that need it.
Generated Dialog/Sheet/Drawer/... start with "use client".
Generated Card/Badge/Button/... do NOT (they are RSC-safe).
rsc: false -> CLI prepends "use client" to EVERY generated component.
Use this when the project is not App Router (Pages Router,
Vite + React, Astro client islands).
ALWAYS set rsc: true for App Router projects. NEVER hand-edit the
directive after add ; re-run shadcn add <name> if the directive
appears wrong.
Quick Reference : Where the Directive Goes
"use client"
import * as React from "react"
import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog"
// ... rest of the file
ALWAYS place "use client" at the very TOP of the file, BEFORE any
import. The directive is FILE-LEVEL ; it applies to every export in the
file. NEVER try to mark a single function or component within a file as
client-only ; the directive does not support that and the build will
silently treat the whole module as a client module.
NEVER quote the directive with backticks. ALWAYS use double quotes or
single quotes : "use client" or 'use client'. A template literal or
a conditional is silently ignored by the bundler.
Decision Tree : Does This File Need "use client" ?
Does the file ...
1. Import a hook (useState, useEffect, useReducer, useRef, useContext,
useMemo, useCallback, useTransition, useDeferredValue, useId,
useSyncExternalStore, custom hooks) ?
YES -> "use client"
2. Import a shadcn primitive from the CLIENT-REQUIRED list above ?
YES -> "use client"
3. Define an event handler (onClick, onChange, onSubmit, onKeyDown, ...) ?
YES -> "use client"
4. Access browser globals (window, document, localStorage,
navigator, IntersectionObserver, ResizeObserver) ?
YES -> "use client"
5. Use a Context Provider (ThemeProvider, QueryClientProvider, ...) ?
YES -> "use client" (the WRAPPER, not the consumer page)
6. Render a server-async component (async function default export with
a server-side fetch) ?
YES -> the page stays SERVER ;
extract any interactive
children into a "use client"
child file.
7. None of the above ? NO -> leave it RSC-safe.
Pattern : Server Shell + Client Island
The canonical App Router page : data on the server, interaction on the
client, no "use client" on the page itself.
// app/products/page.tsx (SERVER)
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { ProductActions } from "./product-actions" // client island
import { db } from "@/lib/db"
export default async function ProductsPage() {
const products = await db.product.findMany()
return (
<div className="grid gap-4">
{products.map((p) => (
<Card key={p.id}>
<CardHeader><CardTitle>{p.name}</CardTitle></CardHeader>
<CardContent>
<p>{p.description}</p>
<ProductActions productId={p.id} /> {/* "use client" boundary lives in this file */}
</CardContent>
</Card>
))}
</div>
)
}
The Card family stays in the server module because it is RSC-safe.
The interactive bits (delete buttons, popover menus, ...) live in
product-actions.tsx which carries the directive.
See references/examples.md for the full file body of
product-actions.tsx, the ThemeProvider wrapper, a TanStack Query
provider wrapper, and a Form-in-server-page recipe.
Pattern : Provider Wrapper (next-themes, TanStack Query, jotai, ...)
Every React context Provider lives in a thin one-purpose client file :
// components/theme-provider.tsx ("use client")
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import type * as React from "react"
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
// app/layout.tsx (SERVER)
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
)
}
ALWAYS put "use client" on the WRAPPER, NEVER on layout.tsx. The
layout stays a server component and STILL renders the client provider
because server components may RENDER client components as descendants ;
the inverse (client renders server) requires the children-prop pattern.
ALWAYS add suppressHydrationWarning on <html> when using next-themes
to avoid React 18+ FOUC warnings (verified, recurring as issue #5552 on
the shadcn-ui repo with 99 reactions). The directive is NOT a workaround
for missing "use client" ; it is a separate fix for the legitimate
class-mismatch on first paint.
Pattern : Passing Data Across the Boundary
SERVER passes to CLIENT : only serialisable data
(string, number, boolean, plain object, plain array,
Date, Map, Set since React 19, null, undefined).
SERVER passes to CLIENT : NEVER class instances, functions, symbols,
Promises, JSX with onClick props.
CLIENT renders SERVER : ONLY via the `children` slot. Pass server JSX
as a children prop from a server parent.
// server page
<ClientIsland data={serialisedProduct} /> {/* OK */}
<ClientIsland => save()} /> {/* COMPILE ERROR : Functions cannot be passed to Client Components */}
<ClientIsland>{<ServerChild />}</ClientIsland> {/* OK : children slot */}
Pass DATA in, host SERVER JSX via children, return interaction
HANDLERS as Server Actions ("use server") inside form action props or
returned from server modules.
Pattern : Forms in a Server Page
The Form primitive (react-hook-form) is always a client component.
Drop it inside the server page :
// app/contact/page.tsx (SERVER)
import { ContactForm } from "./contact-form"
export default function ContactPage() {
return (
<main>
<h1>Contact</h1>
<ContactForm /> {/* "use client" inside */}
</main>
)
}
ContactForm carries the directive ; the surrounding <h1> and the
page wrapper stay server-rendered, so only the form bundle ships.
For full code see references/examples.md §5.
Anti-Patterns
See references/anti-patterns.md for the full catalogue. Top 6 :
"use client"on the entireapp/layout.tsx"to be safe" : every page becomes a client page ; you have just deleted RSC from the project. Move the directive to the smallest leaf.- Importing Dialog into a server component without a client wrapper.
Build fails with
useState is not a functionbecause Radix Dialog callsReact.useStateduring render. - Forgetting
"use client"on the next-themes ThemeProvider wrapper. Hydration error :useTheme is not a functionbecause the hook resolves to its server stub. - Server component passing an
onClickhandler to a client child.Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". - Client component receiving a non-serialisable prop (class instance, Map of class instances, Date with extra prototype methods). Silent mis-render after hydration.
- Conditional or template-literal directive (
`use client`, orif (...) "use client"somewhere mid-file). The bundler treats this as a normal string and the file ships as a server module ; the Radix import then throws at render.
When to Hand Off to Other Skills
- Designing a new primitive ? ->
shadcn-syntax-{primitive}. - Choosing where to drop a Form ? ->
shadcn-syntax-form. - Composing Dialog + Drawer responsively ? ->
shadcn-impl-responsive-dialog-drawer. - Re-running
shadcn addafter toggling therscflag ? ->shadcn-impl-component-install. - Auditing an existing project's directive
placement ? ->
shadcn-agents-rsc-boundary-validator(B14).
Reference Files
references/methods.md: full 60-row per-primitive RSC matrix +components.jsonrscfield reference.references/examples.md: five worked examples : server-page + Card render, ProductActions client island, ThemeProvider wrapper, TanStack Query provider wrapper, Form-in-server-page.references/anti-patterns.md: six anti-patterns with diagnosis, fix, and verification step.