Skip to content
Back to skills

Composition Patterns Guide

ASecurity

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/`). Tri...

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 19, 2026
developmentgoshellreactrefactoringgitapi

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add lukedj78/dev-flow --skill composition-patterns-guide --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Composition Patterns Guide?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Composition Patterns Guide
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lukedj78-composition-patterns-guide/badge)](https://www.skillsdirectory.com/skills/lukedj78-composition-patterns-guide)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.md11.9 KB
  • references/colocation-rules.md2.3 KB
  • references/contracts.md80.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.