| 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:
- The 7 Vercel
composition-patterns rules (originally from vercel-labs/agent-skills), adapted to our stack.
- 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.
The 7 composition rules (priority order)
Priority 1 — Component Architecture (HIGH)
Rule 1: Avoid boolean prop proliferation
Wrong:
<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):
<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:
function Card({ title, body, footer, showAvatar, avatarUrl }) {
return <div>...</div>;
}
Right:
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;
<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:
<DialogProvider isOpen={isOpen} setIsOpen={setIsOpen} title={title} setTitle={setTitle}>
Right:
<DialogProvider value={dialogStore}> {}
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:
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:
<DataTable renderHeader={() => <Header />} renderRow={(row) => <Row data={row} />} />
Right:
<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:
<Button variant="primary" size="lg">Save</Button>
<Button variant="ghost" size="sm">Cancel</Button>
<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.
Wrong:
const Button = forwardRef<HTMLButtonElement, Props>((props, ref) => (
<button ref={ref} {...props} />
));
Right:
function Button({ ref, ...props }: Props & { ref?: Ref<HTMLButtonElement> }) {
return <button ref={ref} {...props} />;
}
Same for use() instead of useContext():
const ctx = use(MyContext);
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.
Sources