Skip to main content
The Composition Patterns skill from Vercel Engineering provides proven patterns for building flexible React components that scale. Learn how to avoid boolean prop proliferation using compound components, context providers, and explicit composition.

Overview

This skill teaches patterns for:
  • Avoiding boolean prop proliferation - Replace isX flags with composition
  • Compound components - Share state via context instead of prop drilling
  • State management - Decouple implementation from UI components
  • Dependency injection - Define generic interfaces for flexibility
These patterns make codebases easier for both humans and AI agents to work with as they scale.

The Problem: Boolean Props

function Composer({
  onSubmit,
  isThread,
  channelId,
  isDMThread,
  dmId,
  isEditing,
  isForwarding,
}: Props) {
  return (
    <form>
      <Header />
      <Input />
      {isDMThread ? (
        <AlsoSendToDMField id={dmId} />
      ) : isThread ? (
        <AlsoSendToChannelField id={channelId} />
      ) : null}
      {isEditing ? (
        <EditActions />
      ) : isForwarding ? (
        <ForwardActions />
      ) : (
        <DefaultActions />
      )}
      <Footer onSubmit={onSubmit} />
    </form>
  )
}
// Each boolean doubles possible states!
// 7 booleans = 128 possible combinations

Pattern 1: Compound Components

Structure complex components with shared context so subcomponents access state without prop drilling.
const ComposerContext = createContext<ComposerContextValue | null>(null)

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

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>
Consumers explicitly compose exactly what they need. No hidden conditionals.

Pattern 2: Dependency Injection

Define generic interfaces so the same UI works with different state implementations.
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 State

// Works with local state (forward message dialog)
<ForwardMessageProvider>
  <Composer.Frame>
    <Composer.Input />
    <Composer.Submit />
  </Composer.Frame>
</ForwardMessageProvider>

// Works with global synced state (channel composer)
<ChannelProvider channelId="abc">
  <Composer.Frame>
    <Composer.Input />
    <Composer.Submit />
  </Composer.Frame>
</ChannelProvider>
The same Composer.Input component works with both providers because it only depends on the context interface, not the implementation.

Pattern 3: Lift State to Providers

Move state management into providers so sibling components can access state without prop drilling.
function ForwardMessageComposer() {
  const [state, setState] = useState(initialState)
  // How do siblings access this state?
  return <Composer.Frame>...</Composer.Frame>
}

function ForwardMessageDialog() {
  return (
    <Dialog>
      <ForwardMessageComposer />
      <MessagePreview /> {/* Needs composer state! */}
      <DialogActions>
        <ForwardButton /> {/* Needs to call submit! */}
      </DialogActions>
    </Dialog>
  )
}

Custom UI Outside Component

// This button lives OUTSIDE Composer.Frame but can still submit!
function ForwardButton() {
  const { actions } = use(ComposerContext)
  return <Button onPress={actions.submit}>Forward</Button>
}

// This preview lives OUTSIDE Composer.Frame but can read state!
function MessagePreview() {
  const { state } = use(ComposerContext)
  return <Preview message={state.input} attachments={state.attachments} />
}
The provider boundary is what matters—not the visual nesting.

Pattern 4: Explicit Variants

Create explicit variant components instead of one component with many modes.
<Composer
  isThread
  isEditing={false}
  channelId="abc"
  showAttachments
  showFormatting={false}
/>
// Hidden logic, unclear outcome

Implementation

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

function EditMessageComposer({ messageId }: Props) {
  return (
    <EditMessageProvider messageId={messageId}>
      <Composer.Frame>
        <Composer.Input />
        <Composer.Footer>
          <Composer.CancelEdit />
          <Composer.SaveEdit />
        </Composer.Footer>
      </Composer.Frame>
    </EditMessageProvider>
  )
}
Each variant is explicit about:
  • What provider/state it uses
  • What UI elements it includes
  • What actions are available
No boolean prop combinations to reason about. No impossible states.

React 19 APIs

React 19+ only. Skip if using React 18 or earlier.

No More forwardRef

const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
  return <TextInput ref={ref} {...props} />
})

use() Instead of useContext()

const value = useContext(MyContext)
// Cannot be called conditionally

When to Apply

Load this skill when:
  • Refactoring components with many boolean props
  • Building reusable component libraries
  • Designing flexible component APIs
  • Reviewing component architecture
  • Working with compound components or context providers

Benefits

Each boolean prop doubles the number of possible states. 7 booleans = 128 combinations. Composition eliminates this complexity.
<ThreadComposer /> is clearer than <Composer isThread isDMThread={false} />. The code documents what it renders.
Consumers compose exactly what they need. Add new variants without modifying existing components.
Test individual subcomponents and providers independently. Mock state easily via context providers.

Skill Structure

.github/skills/vercel-composition-patterns/
├── SKILL.md                                    # Quick reference
├── AGENTS.md                                   # Full documentation
└── rules/
    ├── architecture-avoid-boolean-props.md
    ├── architecture-compound-components.md
    ├── state-decouple-implementation.md
    ├── state-context-interface.md
    ├── state-lift-state.md
    ├── patterns-explicit-variants.md
    ├── patterns-children-over-render-props.md
    └── react19-no-forwardref.md

References

Start by identifying components with 3+ boolean props. These are prime candidates for refactoring to compound components.

Build docs developers (and LLMs) love