| name | composition-patterns |
| description | Component composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component APIs, or designing reusable compound components. Covers compound components, context providers, state lifting, and explicit variants. Examples use React but the principles apply to any component framework including SolidJS. |
React Composition Patterns
Version 1.0.0
Engineering
January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases using composition. Humans
may also find it useful, but guidance here is optimized for automation
and consistency by AI-assisted workflows.
Abstract
Composition patterns for building flexible, maintainable components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.
Table of Contents
- Component Architecture — HIGH
- State Management — MEDIUM
- Implementation Patterns — MEDIUM
1. Component Architecture
Impact: HIGH
Fundamental patterns for structuring components to avoid prop
proliferation and enable flexible composition.
1.1 Avoid Boolean Prop Proliferation
Impact: CRITICAL (prevents unmaintainable component variants)
Don't add boolean props like isThread, isEditing, isDMThread to customize
component behavior. Each boolean doubles possible states and creates
unmaintainable conditional logic. Use composition instead.
Incorrect: boolean props create exponential complexity
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>
)
}
Correct: composition eliminates conditionals
function ChannelComposer() {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<Composer.Footer>
<Composer.Attachments />
<Composer.Formatting />
<Composer.Emojis />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
function ThreadComposer({ channelId }: { channelId: string }) {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<AlsoSendToChannelField id={channelId} />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
function EditComposer() {
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer>
<Composer.Formatting />
<Composer.Emojis />
<Composer.CancelEdit />
<Composer.SaveEdit />
</Composer.Footer>
</Composer.Frame>
)
}
Each variant is explicit about what it renders. We can share internals without
sharing a single monolithic parent.
1.2 Use Compound Components
Impact: HIGH (enables flexible composition without prop drilling)
Structure complex components as compound components with a shared context. Each
subcomponent accesses shared state via context, not props. Consumers compose the
pieces they need.
Incorrect: monolithic component with render props
function Composer({
renderHeader,
renderFooter,
renderActions,
showAttachments,
showFormatting,
showEmojis,
}: Props) {
return (
<form>
{renderHeader?.()}
<Input />
{showAttachments && <Attachments />}
{renderFooter ? (
renderFooter()
) : (
<Footer>
{showFormatting && <Formatting />}
{showEmojis && <Emojis />}
{renderActions?.()}
</Footer>
)}
</form>
)
}
Correct: compound components with shared context
const ComposerContext = createContext<ComposerContextValue | null>(null)
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
return (
<ComposerContext value={{ state, actions, meta }}>
{children}
</ComposerContext>
)
}
function ComposerFrame({ children }: { children: React.ReactNode }) {
return <form>{children}</form>
}
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>
}
const Composer = {
Provider: ComposerProvider,
Frame: ComposerFrame,
Input: ComposerInput,
Submit: ComposerSubmit,
Header: ComposerHeader,
Footer: ComposerFooter,
Attachments: ComposerAttachments,
Formatting: ComposerFormatting,
Emojis: ComposerEmojis,
}
Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
2. State Management
Impact: MEDIUM
Patterns for lifting state and managing shared context across
composed components.
2.1 Decouple State Management from UI
Impact: MEDIUM (enables swapping state implementations without changing UI)
The provider component should be the only place that knows how state is managed.
UI components consume the context interface—they don't know if state comes from
useState, Zustand, or a server sync.
Incorrect: UI coupled to state implementation
function ChannelComposer({ channelId }: { channelId: string }) {
const state = useGlobalChannelState(channelId)
const { submit, updateInput } = useChannelSync(channelId)
return (
<Composer.Frame>
<Composer.Input
value={state.input}
onChange={(text) => sync.updateInput(text)}
/>
<Composer.Submit onPress={() => sync.submit()} />
</Composer.Frame>
)
}
Correct: state management isolated in provider
function ChannelProvider({
channelId,
children,
}: {
channelId: string
children: React.ReactNode
}) {
const { state, update, submit } = useGlobalChannel(channelId)
const inputRef = useRef(null)
return (
<Composer.Provider
state={state}
actions={{ update, submit }}
meta={{ inputRef }}
>
{children}
</Composer.Provider>
)
}
function ChannelComposer() {
return (
<Composer.Frame>
<Composer.Header />
<Composer.Input />
<Composer.Footer>
<Composer.Submit />
</Composer.Footer>
</Composer.Frame>
)
}
2.2 Define Generic Context Interfaces for Dependency Injection
Impact: HIGH (enables dependency-injectable state across use-cases)
Define a generic interface for your component context with three parts:
state, actions, and meta. This interface is a contract that any provider
can implement—enabling the same UI components to work with completely different
state implementations.
Core principle: Lift state, compose internals, make state dependency-injectable.
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
}
UI components consume the interface, not the implementation. Different providers
implement the same interface for different use-cases (local state for ephemeral
forms, global synced state for channels, etc.).
2.3 Lift State into Provider Components
Impact: HIGH (enables state sharing outside component boundaries)
Move state management into dedicated provider components. This allows sibling
components outside the main UI to access and modify state without prop drilling
or awkward refs.
Incorrect: state trapped inside component
function ForwardMessageComposer() {
const [state, setState] = useState(initialState)
return (
<Composer.Frame>
<Composer.Input />
<Composer.Footer />
</Composer.Frame>
)
}
function ForwardMessageDialog() {
return (
<Dialog>
<ForwardMessageComposer />
<MessagePreview /> {/* Needs composer state */}
<ForwardButton /> {/* Needs to call submit */}
</Dialog>
)
}
Correct: state lifted to provider
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(initialState)
const forwardMessage = useForwardMessage()
const inputRef = useRef(null)
return (
<Composer.Provider
state={state}
actions={{ update: setState, submit: forwardMessage }}
meta={{ inputRef }}
>
{children}
</Composer.Provider>
)
}
function ForwardMessageDialog() {
return (
<ForwardMessageProvider>
<Dialog>
<ForwardMessageComposer />
<MessagePreview />
<DialogActions>
<CancelButton />
<ForwardButton />
</DialogActions>
</Dialog>
</ForwardMessageProvider>
)
}
function ForwardButton() {
const { actions } = use(Composer.Context)
return <Button onPress={actions.submit}>Forward</Button>
}
Key insight: Components that need shared state don't have to be visually
nested inside each other—they just need to be within the same provider.
3. Implementation Patterns
Impact: MEDIUM
Specific techniques for implementing compound components and
context providers.
3.1 Create Explicit Component Variants
Impact: MEDIUM (self-documenting code, no hidden conditionals)
Instead of one component with many boolean props, create explicit variant
components. Each variant composes the pieces it needs. The code documents
itself.
Incorrect: one component, many modes
<Composer
isThread
isEditing={false}
channelId='abc'
showAttachments
showFormatting={false}
/>
Correct: explicit variants
<ThreadComposer channelId="abc" />
<EditMessageComposer messageId="xyz" />
<ForwardMessageComposer messageId="123" />
Each implementation is unique, explicit and self-contained. Yet they can each
use shared parts.
3.2 Prefer Composing Children Over Render Props
Impact: MEDIUM (cleaner composition, better readability)
Use children for composition instead of renderX props. Children are more
readable, compose naturally, and don't require understanding callback
signatures.
When render props are appropriate:
<List
data={items}
renderItem={({ item, index }) => <Item item={item} index={index} />}
/>
Use render props when the parent needs to provide data or state to the child.
Use children when composing static structure.
References
- https://react.dev
- https://react.dev/learn/passing-data-deeply-with-context
- https://react.dev/reference/react/use
- vercel-labs/agent-skills