Component Composition and Controlled vs Uncontrolled Patterns in React
A practical engineering guide to designing flexible React components using composition, controlled patterns, and uncontrolled inputs.
Component Composition and Controlled vs Uncontrolled Patterns in React
Scalable React applications rely on two foundational ideas:
1. Building components through composition instead of configuration.
2. Choosing controlled or uncontrolled patterns intentionally when managing input state.
This guide shows how to design flexible component APIs and how to choose the correct input pattern for real-world interfaces.
1. Why Composition Matters
Composition lets developers assemble UI by combining smaller parts. Instead of placing complexity inside a single component with dozens of props, composition pushes structure outward into the consumer.
Configuration-heavy example:
<Card size="lg" padding="xl" showHeader showFooter center />This is difficult to extend or customize.
Composed version:
<Card>
<Card.Header>
<Title />
</Card.Header>
<Card.Body>
<Content />
</Card.Body>
<Card.Footer>
<Actions />
</Card.Footer>
</Card>Benefits
- No prop explosion
- Flexible layouts
- Extensible without modifying the component
- Easier to test and maintain
2. Slot-based Composition
A slot is a placeholder inside a component that accepts user-provided elements.
function Modal({ children }) {
return <div className="modal">{children}</div>;
}Usage:
<Modal>
<ModalHeader />
<ModalBody>
<Form />
</ModalBody>
<ModalFooter />
</Modal>Slots give end users complete control over structure.
3. Compound Components
Compound components allow multiple components to work together by sharing internal state through context.
Parent:
const TabsContext = createContext(null);
function Tabs({ children }) {
const [active, setActive] = useState(0);
return (
<TabsContext.Provider value={{ active, setActive }}>
{children}
</TabsContext.Provider>
);
}Children:
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ index, children }) {
const { active, setActive } = useContext(TabsContext);
const isActive = active == index;
return (
<button
aria-selected={isActive}
onClick={() => setActive(index)}
>
{children}
</button>
);
}
function TabPanel({ index, children }) {
const { active } = useContext(TabsContext);
if (active != index) return null;
return <div className="tab-panel">{children}</div>;
}4. Controlled Components
Controlled components store their value in React state and update through props.
function NameInput() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}Use controlled inputs when:
- UI must update immediately based on value
- Validation is needed
- State must sync with other parts of the UI
- The value influences component behavior
5. Uncontrolled Components
Uncontrolled components store their value in the DOM.
function FileInput() {
const ref = useRef(null);
function handleSubmit() {
const file = ref.current.files[0];
console.log(file);
}
return (
<>
<input type="file" ref={ref} />
<button onClick={handleSubmit}>Upload</button>
</>
);
}Use uncontrolled inputs when:
- You only read the value when needed (such as on submit)
- You want to avoid re-rendering during typing
- The input uses browser behavior like file uploads or uncontrolled forms
6. Choosing the Right Pattern
Controlled is best when:
- The value needs to be validated
- The UI depends on the input value
- You need real-time updates
- You reset or modify the value programmatically
Uncontrolled is best when:
- The value is only needed on submit
- Performance matters (large forms)
- Using native input behavior
7. Combining Composition with Input Patterns
You can form advanced UIs by combining composition and controlled patterns.
function Form({ onSubmit, children }) {
return <form onSubmit={onSubmit}>{children}</form>;
}
function TextField({ label, value, onChange }) {
return (
<label>
{label}
<input value={value} onChange={onChange} />
</label>
);
}Usage:
<Form onSubmit={handleSubmit}>
<TextField label="Email" value={email} onChange={setEmail} />
<Button>Submit</Button>
</Form>This approach scales without losing clarity.
Final Thoughts
Composition is the most powerful method for building flexible React UIs. Controlled and uncontrolled patterns give you predictable ways to manage input behavior. When combined, they create clean, maintainable, and scalable interfaces for real production applications.