Composition Patterns Guide
ASecurityUse when refactoring React or React Native components that suffer from boolean prop proliferation, missing compound structure, prop drilling, or unclear ownership of state. Codifies the 7 Vercel composition-patterns rules for our web + RN stack — composition or explicit variants over boolean props, compound components with shared context, state lifted into providers, children over render props, React 19 (no forwardRef) — plus our colocation rules (Rule of Three, L0/L1/L2, `_components/`). Tri...
- 6 stars
- 0 votes
- 0 copies
- 1 view
- Added October 19, 2026
Works with
Security analysis
100/100Pro scans all 3 files and shows the line behind each finding
npx -y skills add lukedj78/dev-flow --skill composition-patterns-guide --agent claude-codeAre you the author of Composition Patterns Guide?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/lukedj78-composition-patterns-guide)---
name: composition-patterns-guide
description: 'Use when refactoring React or React Native components that suffer from boolean prop proliferation, missing compound structure, prop drilling, or unclear ownership of state. Codifies the 7 Vercel composition-patterns rules for our web + RN stack — composition or explicit variants over boolean props, compound components with shared context, state lifted into providers, children over render props, React 19 (no forwardRef) — plus our colocation rules (Rule of Three, L0/L1/L2, `_components/`). Triggers on: "refactor this component", "questo componente ha troppi prop booleani", "rendi questo componente più componibile", "compound component", "context provider per X", "design del componente". Not for: scaffolding new components (use design-md-to-app, screenshot-to-page, rn-add-screen), or moving components up the hierarchy (use promote-component).'
---
# composition-patterns-guide — modern React composition + our colocation rules
This skill is a **knowledge guardrail** that activates whenever you're designing, refactoring, or reviewing the architecture of a React/RN component. It combines two layers:
1. **The 7 Vercel `composition-patterns` rules** (originally from `vercel-labs/agent-skills`), adapted to our stack.
2. **Our colocation rules** (Rule of Three for promotion, L0/L1/L2, `_components/` and `components/shared/<dominio>/`).
It does NOT execute refactors — for that, use `promote-component`. It provides the *thinking framework*.
## When this skill applies
- About to add a 4th boolean prop to an existing component.
- About to write a component with `renderHeader`, `renderFooter`, `renderActions` render-prop callbacks.
- A component grew over ~250 lines and needs structural refactor.
- You see prop drilling 3+ levels deep.
- About to use `forwardRef` in new code (React 19+ projects).
- Reviewing a PR that touches component architecture.
Orchestrator does NOT route here automatically — invoked by the agent's own judgment when the patterns match.
## Rule zero — compose the library's primitives (golden rule 3)
Every rule below is about **how** to compose. **What** to compose is fixed by golden rule 3 (`references/contracts.md` §Golden rules): the primitives of the library `meta.json#stack` declares. Before writing a component, search `components/ui/` (shadcn monorepo: `packages/ui/src/components/`; mobile: React Native / Expo core components and `components/shared/`) for a primitive that covers the pattern; if it is there, use it, and if two together cover it, compose them. Never hand-roll one the library ships, never import a second component library, never change a primitive beyond its `cva` variants and tokens. A compound component here is built *from* those primitives (`Card`, `Tabs`, `Dialog`…), never a parallel `<Tabs.Root>` of your own; a truly new primitive needs a recorded exception with its reason (ADR or `meta.json#stack_config.primitive_exceptions`).
**The primitive renders the design.** A compound component's parts are primitives too, so a `Composer.Header` is a `CardHeader` or an `Item`, not a `div` with `rounded-xl border`, and a `Composer.Action` is a `Button`, not a `<button>`. A raw element or a `div`/`span` whose classes redraw a Card, Badge, Alert or Separator is a golden-rule-3 violation even with no import: the lint rule **`design/no-handmade-ui`** reports it with the primitive to use, and on web projects with a Tailwind UI the **write-time guard** (`scripts/guard/primitives-guard.mjs`, a Claude Code hook) refuses the write. When a part must look different from the primitive, tune the primitive — a `cva` variant recorded first in DESIGN.md, the tokens it reads — never classes on the part that rebuild the look; when no primitive can render it without a fork, stop and ask the user. Pattern → primitive: `design-md-to-app/references/primitives-first.md`.
## The 7 composition rules (priority order)
### Priority 1 — Component Architecture (HIGH)
#### Rule 1: Avoid boolean prop proliferation
**Wrong**:
```tsx
<Composer
onSubmit={...}
isThread={true}
channelId={42}
isDMThread={false}
dmId={null}
isEditing={true}
isForwarding={false}
/>
```
Each boolean doubles the state-space. 5 booleans = 32 possible combinations, most of which are invalid or untested.
**Right** (composition):
```tsx
<Composer>
<Composer.Header />
<Composer.Input />
<Composer.ThreadField channelId={42} />
<Composer.EditActions />
<Composer.Footer onSubmit={...} />
</Composer>
```
The consumer composes the parts they need. No invalid states.
#### Rule 2: Use compound components
A complex component should expose **subcomponents** that share context, not a single API with N props.
**Wrong**:
```tsx
function Card({ title, body, footer, showAvatar, avatarUrl }) {
return <div>...</div>;
}
```
**Right**:
```tsx
function Card({ children }) {
return <div className="card">{children}</div>;
}
function CardHeader({ children }) { return <div className="card-header">{children}</div>; }
function CardBody({ children }) { return <div className="card-body">{children}</div>; }
Card.Header = CardHeader;
Card.Body = CardBody;
// usage:
<Card>
<Card.Header><Avatar src="..." /><h2>Title</h2></Card.Header>
<Card.Body>...</Card.Body>
</Card>
```
If the subcomponents share state (e.g., a Disclosure that opens/closes), wrap in a context provider.
### Priority 2 — State Management (MEDIUM)
#### Rule 3: Decouple state from implementation
A provider should expose `{ state, actions, meta }` — a generic interface — not surface every state variable as a separate prop.
**Wrong**:
```tsx
<DialogProvider isOpen={isOpen} setIsOpen={setIsOpen} title={title} setTitle={setTitle}>
```
**Right**:
```tsx
<DialogProvider value={dialogStore}> {/* zustand/jotai/use-state hook returns {state, actions, meta} */}
```
Then consumers use `useDialog()` and get the same interface regardless of how the state is stored.
#### Rule 4: Lift state into provider components
When siblings need to share state, move it into a provider that wraps both. Don't drill props through their parent.
#### Rule 5: Context interface — `{state, actions, meta}` shape
The provider's context value follows a predictable shape:
```ts
type CardContext = {
state: { isExpanded: boolean };
actions: { toggle: () => void };
meta: { id: string };
};
```
This makes contexts mockable in tests (`mockContext({state: {...}, actions: {...}})`) and substitutable in Storybook.
### Priority 3 — Implementation Patterns (MEDIUM)
#### Rule 6: Children over render props
**Wrong**:
```tsx
<DataTable renderHeader={() => <Header />} renderRow={(row) => <Row data={row} />} />
```
**Right**:
```tsx
<DataTable>
<DataTable.Header />
{rows.map(r => <DataTable.Row key={r.id} data={r} />)}
</DataTable>
```
JSX as children is more readable, more compose-able, and supports tooling (highlighting, refactor, ref forwarding) better than render props.
#### Rule 7: Explicit variants (sometimes)
For variants that change visual identity significantly, create explicit named exports:
```tsx
// Instead of:
<Button variant="primary" size="lg">Save</Button>
<Button variant="ghost" size="sm">Cancel</Button>
// Consider:
<PrimaryButton>Save</PrimaryButton>
<GhostButton size="sm">Cancel</GhostButton>
```
⚠️ Caveat: shadcn / Radix use `variant` props heavily (via `cva`) and that's idiomatic in those libraries. The "explicit variants" rule applies more to *application-level* components than primitives.
### Priority 4 — React 19 APIs (MEDIUM, only React 19+)
#### Rule 8: No forwardRef
In React 19, `ref` is a normal prop. Don't use `forwardRef` for new code. (The snippets below are the shape of a primitive **inside `components/ui/`** — the one place a raw `<button>` belongs; application code imports that `Button`.)
**Wrong**:
```tsx
const Button = forwardRef<HTMLButtonElement, Props>((props, ref) => (
<button ref={ref} {...props} />
));
```
**Right**:
```tsx
function Button({ ref, ...props }: Props & { ref?: Ref<HTMLButtonElement> }) {
return <button ref={ref} {...props} />;
}
```
Same for `use()` instead of `useContext()`:
```tsx
const ctx = use(MyContext); // React 19+, supports conditional reads
```
## Server/Client Component boundary (App Router)
Composition decisions above interact with the Server/Client split — get this wrong and a compound component either forces the whole tree client-side or fails to render at all:
- **A compound's root/provider is a Client Component the moment it needs `useState`/context for shared state** (Rule 4/5) — mark it `"use client"`. That does **not** force its subcomponents client-side too: a Server Component can still be passed as `children` into a Client Component's slot (e.g., `<ClientProvider>{await ServerFetchedContent()}</ClientProvider>` composed from a parent Server Component) — React keeps the Server-rendered subtree server-rendered even though the wrapping provider is a Client Component.
- **Don't add `"use client"` to a compound component just because one subcomponent needs interactivity.** Push the directive down to the smallest leaf that actually needs it (the button, the toggle), not up to the compound root — this preserves rung-1 colocation and avoids client-bundling the whole component for one `onClick`.
- **`children`-over-render-props (Rule 6) is also the standard way to keep a Server Component inside a Client Component's layout**: the Client Component (e.g., a `<Tabs>` shell needing `useState` for the active tab) renders `{children}` it received from a Server Component parent, rather than importing and rendering Server-fetched content itself (a Client Component cannot `import` and render a Server Component directly — only receive one as a prop/children from above).
## Our colocation rules (briefly — full spec in `references/colocation-rules.md`)
| Level | Path | Use case |
|---|---|---|
| L0 (default) | `app/<route>/_components/<Name>.tsx` | Used by 1 page only |
| L1 | `app/(group)/_components/<Name>.tsx` | Used by 2+ pages of same route group |
| L2 | `components/shared/<dominio>/<Name>.tsx` | Used by pages of multiple route groups |
| UI primitives | `components/ui/<name>.tsx` | shadcn/Base UI/MUI primitives |
| Theme system | `components/theme/<name>.tsx` | ThemeProvider, ModeToggle |
**Rule of Three**: stay at L0 until 3rd use, then promote. Use `promote-component` skill to automate.
## React Native specifics
- **Compound components** work identically. `<Card.Root>` + `<Card.Header>` in NativeWind, no API difference.
- **No `forwardRef`** issue (RN is React 19+ since 2024).
- **Don't lift state to `store/` automatically**: a Zustand store is for *cross-feature* state. Within a single feature, context + `useState` is correct.
- **Compound + NativeWind**: each part of a compound can have its own `className` prop. NativeWind v4 handles the compositions cleanly.
## Anti-patterns this skill flags
- ❌ 4+ boolean props on a single component → suggest compound or explicit variants.
- ❌ `renderHeader` / `renderFooter` / `renderXxx` render props → suggest children or compound.
- ❌ State managed by parent's `useState` then drilled through 3+ levels → suggest lifting to provider.
- ❌ `forwardRef` in React 19+ code → suggest plain `ref` prop.
- ❌ `useContext` in React 19+ for conditional access → suggest `use()`.
- ❌ Compound components split across files at different colocation levels → suggest unifying.
- ❌ Premature promotion of a component to L2 at the 2nd use → suggest waiting + duplicating.
- ❌ A component that re-implements a primitive already in `components/ui/` (a custom dropdown, modal, tabs, tooltip, skeleton) → golden rule 3: use or compose the primitive; a new one only as a recorded exception.
## Sources
- Vercel `composition-patterns` skill: https://github.com/vercel-labs/agent-skills/tree/main/skills/composition-patterns
- Spec: `docs/superpowers/specs/2026-06-06-folder-structure-refactor.md`
- Sandi Metz, "The Wrong Abstraction": https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
- React 19 docs: https://react.dev/reference/react
Files in this skill
- SKILL.md
- references/colocation-rules.md
- references/contracts.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.