shadcn ui : RSC Boundary Validator (Agent / Validator)
This skill is an AGENT skill. It does NOT teach how to compose RSC vs
client trees (that is shadcn-impl-rsc-vs-client-boundaries, B10). It
teaches an automated AUDIT : given a project tree, classify every shadcn
import, verify the "use client" directive is in the right files, in
the right form, in the right place, and that the boundary crossing
follows the React Server Components rules.
ALWAYS run the validator as a six-point checklist in a fixed order. NEVER stop at the first failure ; a project usually has several boundary defects layered together, and the checklist surfaces all of them in one pass.
Companion Skills
shadcn-impl-rsc-vs-client-boundaries(B10) : the rulebook that defines WHICH primitives need the directive and WHY ; this validator enforces those rules. The full per-primitive matrix lives in B10references/methods.md(60 rows).shadcn-core-cli:components.jsonschema including therscflag,shadcn add --overwritefor regenerating a file whose directive is wrong.shadcn-impl-component-install: the install workflow that emits files with or without the directive ; use this skill if the validator reports a missing-directive file and you want to re-add cleanly.shadcn-impl-framework-integration: per-framework defaults (App Router ->rsc:true, Vite ->rsc:false, Astro client island ->rsc:false).
Quick Reference : Validator Workflow
INPUT : a repo path with components.json + components/ui/* + app/ (or pages/)
PHASE 1 : scan components.json -> read `rsc` flag, detect framework
PHASE 2 : classify every file under components/ui/* and app/ using
the per-primitive matrix (RSC-safe / client-required / mixed)
PHASE 3 : verify the `"use client"` directive is present where required,
absent where forbidden, well-formed where present
PHASE 4 : scan for boundary-violation props (onClick / function / class
instance passed from server file to client file)
PHASE 5 : scan for Provider-in-server-layout anti-pattern
PHASE 6 : emit a verdict report : PASS / WARN / FAIL with file + line
OUTPUT : 6-point checklist with PASS / FAIL per item + remediation hint
ALWAYS run all six phases even on small projects ; the cheap phases (grep) cost milliseconds and catch the silently-broken cases.
Quick Reference : 6-Point Checklist
[1] components.json `rsc` flag matches the framework :
App Router -> rsc:true.
Pages Router / Vite / Astro / Remix / TanStack Start -> rsc:false.
[2] Every file importing a CLIENT-REQUIRED primitive starts with the
directive :
Dialog / Sheet / Drawer / AlertDialog / Popover / Tooltip /
HoverCard / Form / Select / Combobox / Command / Sonner (Toaster
mount) / Tabs / Accordion / Carousel / Sidebar / Calendar /
DropdownMenu / ContextMenu / Menubar / NavigationMenu / Switch /
Slider / Checkbox / RadioGroup / Collapsible / ScrollArea /
InputOTP / Toggle / ToggleGroup / Resizable / Chart / Progress.
[3] No `app/layout.tsx` / `app/page.tsx` / `app/template.tsx` carries
the directive (defeats RSC for the whole subtree).
[4] Every `"use client"` is :
- the FIRST non-whitespace / non-comment line of the file,
- double-quoted or single-quoted (NEVER backticks, NEVER a
template literal, NEVER inside an `if`),
- file-level (no per-function or per-component attempt).
[5] Every context Provider (next-themes ThemeProvider, TanStack Query
QueryClientProvider, jotai Provider, zustand Provider, Sonner
Toaster, OverlayProvider, NuqsAdapter) lives in a dedicated
`"use client"` wrapper file ; never imported directly into
`app/layout.tsx` or any server component.
[6] No server file (no `"use client"` directive) passes a function
(`onClick`, `onChange`, `onSubmit`, `onSelect`, ...) or a non-
serialisable instance (class instance, Decimal, Date with custom
methods, Map<key, classInstance>) to a client child.
For ripgrep / find one-liners that implement each check see
references/methods.md §3.
Quick Reference : Per-Primitive Lookup Matrix
The validator inherits the canonical per-primitive table from
shadcn-impl-rsc-vs-client-boundaries references/methods.md §3
(60 rows). The condensed lookup used by this validator :
CLIENT-REQUIRED (must carry "use client") :
Accordion, AlertDialog, Calendar, Carousel, Chart, Checkbox,
Collapsible, Combobox, Command, ContextMenu, Dialog, Drawer,
DropdownMenu, Form (react-hook-form), HoverCard, InputOTP, Menubar,
NavigationMenu, Popover, Progress, RadioGroup, Resizable,
ScrollArea, Select, Sheet, Sidebar, Slider, Sonner Toaster,
Switch, Tabs, Toggle, ToggleGroup, Tooltip.
RSC-SAFE (must NOT carry "use client" unless the parent file also
imports a client primitive) :
Alert, AspectRatio, Avatar, Badge, Breadcrumb, Button, Card,
Empty, Field, Input, InputGroup, Item, Kbd, Label, Native Select
(without onChange), Pagination, Separator, Skeleton, Spinner,
Table primitives (Table / TableRow / ...), Textarea, Typography.
MIXED (the export bundle includes both a client root and pure-HTML
subcomponents ; the emitted file is client) :
AlertDialog (Trigger + Content + body), Direction.
Rule of thumb the validator uses : if the source file imports anything
from @radix-ui/* (other than @radix-ui/react-label and @radix-ui/ react-separator) or from cmdk / embla-carousel-react / vaul /
react-day-picker / react-resizable-panels / react-hook-form /
sonner / @tanstack/react-table / recharts, the file MUST carry
the directive. See references/methods.md §2 for the full grep
pattern and §5 for the matrix snapshot.
Quick Reference : Three Verdict Levels
PASS : all six checks succeed ; the project's RSC boundary is sound.
WARN : at least one rule fired on a RSC-safe primitive (cosmetic
over-clientification : Card / Badge / Button with the directive
for no reason ; ships extra JavaScript but does not crash).
FAIL : at least one rule fired on a CLIENT-REQUIRED primitive (the
project WILL crash at render with `useState is not a function`
or similar) ; a Provider was found in a server layout ; a
server file passed a function to a client child ; the `rsc`
flag mismatches the framework.
ALWAYS report ALL failures in a single verdict ; never short-circuit.
Decision Tree : Verifying a Single File
For file F :
1. Read first non-comment, non-blank line.
2. If F is `app/layout.tsx` / `app/page.tsx` / `app/template.tsx` :
and line 1 == `"use client"` -> FAIL (rule 3)
3. Else if line 1 == `"use client"` :
verify the directive is well-formed (rule 4)
mark F as a CLIENT file ; continue.
4. Else (no directive on line 1) :
scan imports for CLIENT-REQUIRED primitives or for any
`@radix-ui/*` package (minus Label / Separator) or any
cmdk / vaul / embla / react-day-picker / react-hook-form /
sonner / @tanstack/react-table / recharts dependency.
If any match -> FAIL (rule 2 ; directive missing).
If only RSC-safe primitives -> file is OK as server.
5. For every JSX child element rendered in F :
if the element is a known client component (imported from a
"use client" file) AND F is server (no directive) AND a
prop on that element is a function literal, an arrow function,
a class instance, a Date with custom methods, or a Map ->
FAIL (rule 6).
See references/examples.md for two worked traces (broken vs
correct) of this decision tree.
Pattern : Running the Validator
# Tier 1 : two-second grep audit. Run in every shadcn project before merge.
grep -L '^"use client"' \
components/ui/{accordion,alert-dialog,calendar,carousel,chart,checkbox,collapsible,combobox,command,context-menu,dialog,drawer,dropdown-menu,form,hover-card,input-otp,menubar,navigation-menu,popover,progress,radio-group,resizable,scroll-area,select,sheet,sidebar,slider,sonner,switch,tabs,toggle,toggle-group,tooltip}.tsx 2>/dev/null
# Expected output : EMPTY. Any filename printed is FAIL.
grep -l '^"use client"' \
components/ui/{alert,aspect-ratio,avatar,badge,breadcrumb,button,card,empty,field,input,input-group,item,kbd,label,pagination,separator,skeleton,spinner,table,textarea}.tsx 2>/dev/null
# Expected output : EMPTY. Any filename printed is WARN.
grep -l '^"use client"' app/layout.tsx app/page.tsx app/template.tsx 2>/dev/null
# Expected output : EMPTY. Any filename printed is FAIL (rule 3).
jq '.rsc' components.json
# Expected : true for App Router. false for Vite / Pages / Astro / Remix.
ALWAYS treat the grep output as authoritative for rules 2, 3, and the
RSC-safe over-clientification check (WARN). For rules 4 (well-formed
directive), 5 (Provider wrappers), and 6 (boundary crossing) use the
extended ripgrep patterns documented in references/methods.md §3.
Pattern : Reporting Format
RSC Boundary Audit : <repo-name>
[1] components.json rsc flag : PASS (rsc:true matches framework=app-router)
[2] Client primitive directives : FAIL
MISSING in components/ui/dialog.tsx (imports @radix-ui/react-dialog)
MISSING in components/ui/sheet.tsx (imports @radix-ui/react-dialog)
[3] Layout / page / template not client : FAIL
OVER-CLIENT app/layout.tsx
[4] Directive well-formedness : PASS (all 12 directives valid)
[5] Provider wrappers : FAIL
ThemeProvider imported directly from next-themes in app/layout.tsx
Wrap in components/theme-provider.tsx with "use client"
[6] Boundary props : PASS (no inline functions across server->client)
Verdict : FAIL. 3 critical defects + 0 warnings.
Run remediation : see shadcn-impl-rsc-vs-client-boundaries B10.
ALWAYS print a section per rule with a single PASS / WARN / FAIL keyword and the offending paths + reason. NEVER aggregate ; per-file detail is what the developer needs to fix.
Pattern : Remediation Hand-Off
After a FAIL verdict the validator does NOT auto-fix. It hands off :
| Defect class | Remediation skill |
|---|---|
Rule 1 : rsc flag wrong |
shadcn-core-cli : edit components.json, rerun add |
| Rule 2 : missing directive on client file | shadcn-impl-component-install : shadcn add <name> --overwrite |
| Rule 3 : layout marked client | shadcn-impl-rsc-vs-client-boundaries §"Server Shell + Client Island" |
| Rule 4 : directive form wrong | manual fix : move to first line, double-quote it |
| Rule 5 : Provider in server layout | shadcn-impl-rsc-vs-client-boundaries §"Provider Wrapper" |
| Rule 6 : function/instance across boundary | shadcn-impl-rsc-vs-client-boundaries §"Passing Data Across the Boundary" |
ALWAYS forward to the skill ; NEVER inline the remediation steps in the validator output. Keeps the validator small and the rulebook authoritative.
When This Skill Does NOT Apply
- The project is a Pages Router project AND
rsc:falseis set : the emitted directive on every file is correct ; rule 2 still applies (Dialog must have it), rule 3 does not (there is noapp/layout). Run rules 1, 2, 4, 5, 6 ; skip rule 3. - The project is a Vite + React SPA : there are no server components ;
every file is client ; rule 3 does not apply. The
rsc:falseflag must be set or the CLI omits the directive on Dialog and the app silently mis-bundles. Run rules 1, 2, 4 ; skip 3 ; rules 5 and 6 become advisory (Provider hygiene is still recommended). - The project is a single-file demo or a Storybook story : the validator is overkill ; rely on the build error instead.
Reference Files
references/methods.md: full ripgrep / find / jq one-liners per rule, per-primitive matrix snapshot (60 rows), AST patterns for rule 6 (server-to-client prop crossing), and a Bash driver script that runs all six rules and emits the report block above.references/examples.md: two worked traces : a BROKEN App Router project (layout client + missing Dialog directive + ThemeProvider in layout + onClick across boundary) with verdict, and the CORRECT refactored version showing the canonical server-shell + client- island layout.references/anti-patterns.md: six anti-patterns the validator catches with diagnosis, fix, and verification step. Covers the six checklist failure modes plus the suppressHydrationWarning-as- bandaid trap.