Main Site ↗

vercel-composition-patterns

by vercel-labs24.2k1.6kGitHub

React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture.

Unlock Deep Analysis

Use AI to visualize the workflow and generate a realistic output preview for this skill.

Powered by Fastest LLM

Development
Compatible Agents
Claude Code
Claude Code
~/.claude/skills/
Codex CLI
Codex CLI
~/.codex/skills/
Gemini CLI
Gemini CLI
~/.gemini/skills/
O
OpenCode
~/.opencode/skills/
O
OpenClaw
~/.openclaw/skills/
GitHub Copilot
GitHub Copilot
~/.copilot/skills/
Cursor
Cursor
~/.cursor/skills/
W
Windsurf
~/.codeium/windsurf/skills/
C
Cline
~/.cline/skills/
R
Roo Code
~/.roo/skills/
K
Kiro
~/.kiro/skills/
J
Junie
~/.junie/skills/
A
Augment Code
~/.augment/skills/
W
Warp
~/.warp/skills/
G
Goose
~/.config/goose/skills/
SKILL.md

React Composition Patterns

Build flexible, maintainable React components using compound components, context providers, and explicit variants. Avoid boolean prop proliferation.

WHAT

Composition patterns that scale:

  • Compound components with shared context
  • State/actions/meta context interface for dependency injection
  • Explicit variant components over boolean props
  • Lifted state in provider components
  • Children composition over render props

WHEN

  • Refactoring components with many boolean props
  • Building reusable component libraries
  • Designing flexible component APIs
  • Creating compound components (Card, Dialog, Form, etc.)
  • Components need shared state across sibling elements

KEYWORDS

composition, compound components, context, provider, boolean props, variants, react patterns, component architecture, render props, children

Source: Vercel Engineering

Installation

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install composition-patterns

Core Principle

Avoid boolean prop proliferation. Each boolean doubles possible states.

// BAD: 4 booleans = 16 possible states
<Composer isThread isDMThread isEditing isForwarding />

// GOOD: Explicit variants, clear intent
<ThreadComposer channelId="abc" />
<EditComposer messageId="xyz" />

Pattern 1: Compound Components

Structure complex components with shared context. Consumers compose what they need.

const ComposerContext = createContext<ComposerContextValue | null>(null)

// Provider handles state
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
  return (
    <ComposerContext value={{ state, actions, meta }}>
      {children}
    </ComposerContext>
  )
}

// Subcomponents access context
function ComposerInput() {
  const { state, actions: { update }, meta: { inputRef } } = use(ComposerContext)
  return (
    <TextInput
      ref={inputRef}
      value={state.input}
      onChangeText={(text) => update(s => ({ ...s, input: text }))}
    />
  )
}

function ComposerSubmit() {
  const { actions: { submit } } = use(ComposerContext)
  return <Button onPress={submit}>Send</Button>
}

// Export as namespace
const Composer = {
  Provider: ComposerProvider,
  Frame: ComposerFrame,
  Input: ComposerInput,
  Submit: ComposerSubmit,
  Header: ComposerHeader,
  Footer: ComposerFooter,
}

Usage:

<Composer.Provider state={state} actions={actions} meta={meta}>
  <Composer.Frame>
    <Composer.Header />
    <Composer.Input />
    <Composer.Footer>
      <Composer.Formatting />
      <Composer.Submit />
    </Composer.Footer>
  </Composer.Frame>
</Composer.Provider>

Pattern 2: Generic Context Interface

Define a contract any provider can implement: state, actions, meta.

interface ComposerState {
  input: string
  attachments: Attachment[]
  isSubmitting: boolean
}

interface ComposerActions {
  update: (updater: (state: ComposerState) => ComposerState) => void
  submit: () => void
}

interface ComposerMeta {
  inputRef: React.RefObject<TextInput>
}

interface ComposerContextValue {
  state: ComposerState
  actions: ComposerActions
  meta: ComposerMeta
}

Same UI, different providers:

// Local state provider
function ForwardMessageProvider({ children }) {
  const [state, setState] = useState(initialState)
  return (
    <ComposerContext value={{
      state,
      actions: { update: setState, submit: useForwardMessage() },
      meta: { inputRef: useRef(null) },
    }}>
      {children}
    </ComposerContext>
  )
}

// Global synced state provider  
function ChannelProvider({ channelId, children }) {
  const { state, update, submit } = useGlobalChannel(channelId)
  return (
    <ComposerContext value={{
      state,
      actions: { update, submit },
      meta: { inputRef: useRef(null) },
    }}>
      {children}
    </ComposerContext>
  )
}

Both work with the same <Composer.Input /> component.


Pattern 3: Explicit Variants

Create named components for each use case instead of boolean modes.

// BAD: What does this render?
<Composer
  isThread
  isEditing={false}
  channelId="abc"
  showAttachments
/>

// GOOD: Self-documenting
<ThreadComposer channelId="abc" />

Implementation:

function ThreadComposer({ channelId }: { channelId: string }) {
  return (
    <ThreadProvider channelId={channelId}>
      <Composer.Frame>
        <Composer.Input />
        <AlsoSendToChannelField channelId={channelId} />
        <Composer.Footer>
          <Composer.Formatting />
          <Composer.Submit />
        </Composer.Footer>
      </Composer.Frame>
    </ThreadProvider>
  )
}

function EditComposer({ messageId }: { messageId: string }) {
  return (
    <EditProvider messageId={messageId}>
      <Composer.Frame>
        <Composer.Input />
        <Composer.Footer>
          <Composer.CancelEdit />
          <Composer.SaveEdit />
        </Composer.Footer>
      </Composer.Frame>
    </EditProvider>
  )
}

Pattern 4: Lifted State

Components outside the visual hierarchy can access state via provider.

function ForwardMessageDialog() {
  return (
    <ForwardMessageProvider>
      <Dialog>
        {/* Composer UI */}
        <Composer.Frame>
          <Composer.Input placeholder="Add a message" />
          <Composer.Footer>
            <Composer.Formatting />
          </Composer.Footer>
        </Composer.Frame>

        {/* Preview OUTSIDE composer but reads its state */}
        <MessagePreview />

        {/* Actions OUTSIDE composer but can submit */}
        <DialogActions>
          <CancelButton />
          <ForwardButton />
        </DialogActions>
      </Dialog>
    </ForwardMessageProvider>
  )
}

// Can access context despite being outside Composer.Frame
function ForwardButton() {
  const { actions: { submit } } = use(ComposerContext)
  return <Button onPress={submit}>Forward</Button>
}

function MessagePreview() {
  const { state } = use(ComposerContext)
  return <Preview message={state.input} attachments={state.attachments} />
}

Key insight: Provider boundary matters, not visual nesting.


Pattern 5: Children Over Render Props

Use children for composition, render props only when passing data.

// BAD: Render props for structure
<Composer
  renderHeader={() => <CustomHeader />}
  renderFooter={() => <Formatting />}
  renderActions={() => <Submit />}
/>

// GOOD: Children for structure
<Composer.Frame>
  <CustomHeader />
  <Composer.Input />
  <Composer.Footer>
    <Formatting />
    <Submit />
  </Composer.Footer>
</Composer.Frame>

When render props ARE appropriate:

// Passing data to children
<List
  data={items}
  renderItem={({ item, index }) => <Item item={item} index={index} />}
/>

Pattern 6: Decouple State from UI

Only the provider knows how state is managed. UI consumes the interface.

// BAD: UI coupled to state implementation
function ChannelComposer({ channelId }) {
  const state = useGlobalChannelState(channelId)  // Knows about global state
  const { submit } = useChannelSync(channelId)    // Knows about sync
  
  return <Composer.Input value={state.input} onChange={...} />
}

// GOOD: State isolated in provider
function ChannelProvider({ channelId, children }) {
  const { state, update, submit } = useGlobalChannel(channelId)
  
  return (
    <Composer.Provider
      state={state}
      actions={{ update, submit }}
      meta={{ inputRef: useRef(null) }}
    >
      {children}
    </Composer.Provider>
  )
}

// UI only knows the interface
function ChannelComposer() {
  return (
    <Composer.Frame>
      <Composer.Input />  {/* Works with any provider */}
      <Composer.Submit />
    </Composer.Frame>
  )
}

Quick Reference

Anti-PatternSolution
Boolean propsExplicit variant components
Render props for structureChildren composition
State in componentLift to provider
Coupled to state implGeneric context interface
Many conditional rendersCompose pieces explicitly

Files

  • rules/architecture-avoid-boolean-props.md - Detailed boolean prop guidance
  • rules/architecture-compound-components.md - Compound component pattern
  • rules/state-context-interface.md - Context interface design
  • rules/state-decouple-implementation.md - State isolation
  • rules/state-lift-state.md - Provider pattern
  • rules/patterns-explicit-variants.md - Variant components
  • rules/patterns-children-over-render-props.md - Composition over callbacks

NEVER

  • Add boolean props to customize behavior (use composition)
  • Create components with more than 2-3 boolean mode props
  • Couple UI components to specific state implementations
  • Use render props when children would work
  • Trap state inside components when siblings need access

Source: https://github.com/vercel-labs/agent-skills#skills~composition-patterns

Content curated from original sources, copyright belongs to authors

Grade B
-AI Score
Best Practices
Checking...
Try this Skill

User Rating

USER RATING

0UP
0DOWN
Loading files...

WORKS WITH

Claude Code
Claude
Codex CLI
Codex
Gemini CLI
Gemini
O
OpenCode
O
OpenClaw
GitHub Copilot
Copilot
Cursor
Cursor
W
Windsurf
C
Cline
R
Roo
K
Kiro
J
Junie
A
Augment
W
Warp
G
Goose