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
❌ Boolean Prop Proliferation
✅ Composition
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.
Context Provider
Subcomponents
Export as Compound Component
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.
Generic Interface
Provider A: Local State
Provider B: Global Synced State
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.
❌ State Trapped Inside Component
✅ State Lifted to Provider
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.
❌ What Does This Render?
✅ Immediately Clear
< 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
No Boolean Prop Explosion
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.