Patterns
Composition over configuration
Section titled “Composition over configuration”React’s component model is built on composition. A component takes children and renders them. This sounds obvious until you see how many component libraries get it wrong.
Configuration approach (common in older libraries):
<Dropdown options={["Apple", "Banana", "Cherry"]} onChange={handleSelect} placeholder="Select fruit" searchable={true} clearable={true} multiSelect={false} maxHeight={300} renderOption={(option) => <span>{option}</span>}/>Twelve props to cover every use case. Add a new feature? Add a prop. Need custom rendering? Add a render prop. The API surface grows until nobody can remember all the options.
Composition approach:
<Dropdown onSelect={handleSelect}> <Dropdown.Trigger>Select fruit</Dropdown.Trigger> <Dropdown.Menu> <Dropdown.Search /> <Dropdown.Item value="apple">Apple</Dropdown.Item> <Dropdown.Item value="banana">Banana</Dropdown.Item> <Dropdown.Item value="cherry">Cherry</Dropdown.Item> </Dropdown.Menu></Dropdown>Each piece is explicit. You can see the structure. You can rearrange it. You can drop Dropdown.Search if you don’t want search. You can put custom content inside Dropdown.Item. No props needed for features you compose directly.
Compound components
Section titled “Compound components”The Dropdown example above is a compound component pattern. Multiple components work together, sharing implicit state through context, but presenting a flexible API to consumers.
Here’s how it works inside:
const DropdownContext = createContext(null);
function Dropdown({ children, onSelect }) { const [open, setOpen] = useState(false); const [selected, setSelected] = useState(null);
function select(value) { setSelected(value); setOpen(false); onSelect?.(value); }
return ( <DropdownContext.Provider value={{ open, setOpen, selected, select }}> <div className="dropdown">{children}</div> </DropdownContext.Provider> );}
Dropdown.Trigger = function Trigger({ children }) { const { open, setOpen, selected } = useContext(DropdownContext); return <button onClick={() => setOpen(!open)}>{selected ?? children}</button>;};
Dropdown.Menu = function Menu({ children }) { const { open } = useContext(DropdownContext); if (!open) return null; return <div className="dropdown-menu">{children}</div>;};
Dropdown.Item = function Item({ value, children }) { const { select, selected } = useContext(DropdownContext); return ( <div className={selected === value ? "selected" : ""} onClick={() => select(value)}> {children} </div> );};The context is internal. Consumers never import it directly. They just compose the sub-components in whatever arrangement they need. This pattern is used by Radix UI, Headless UI, Reach UI, and most well-designed component libraries.
Custom hooks as the primary abstraction
Section titled “Custom hooks as the primary abstraction”Before hooks, React had two patterns for logic reuse: Higher-Order Components (HOCs) and render props. Both are effectively dead for new code. Custom hooks replaced them.
HOC (the old way):
function withWindowSize(WrappedComponent) { return function WithWindowSize(props) { const [size, setSize] = useState({ width: window.innerWidth }); useEffect(() => { /* resize listener */ }, []); return <WrappedComponent {...props} windowSize={size} />; };}
const MyComponent = withWindowSize(function MyComponent({ windowSize, ...props }) { // windowSize is injected by the HOC});Problems: prop name collisions, unclear where props come from, stacking multiple HOCs creates wrapper pyramids in DevTools.
Custom hook (the new way):
function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => { function handleResize() { setSize({ width: window.innerWidth, height: window.innerHeight }); } window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []);
return size;}
function MyComponent() { const { width } = useWindowSize(); return width > 768 ? <Desktop /> : <Mobile />;}No wrappers. No injected props. The hook is called directly. You can see exactly where the data comes from by reading the component. And if two hooks return the same-named value, you just destructure with different names.
Custom hooks I’ve written in production:
useDebounce(value, delay)for search inputsuseLocalStorage(key, defaultValue)for persisted preferencesuseMediaQuery(query)for responsive logicuseIntersectionObserver(ref)for lazy loadingusePrevious(value)for comparing current vs previous renders
Each one is 10-30 lines. Each one saves dozens of lines every time it’s used. That’s the power of hooks as an abstraction layer.
Error boundaries
Section titled “Error boundaries”This is the one pattern where you still need a class component. React has no hook equivalent for componentDidCatch.
class ErrorBoundary extends React.Component { state = { hasError: false, error: null };
static getDerivedStateFromError(error) { return { hasError: true, error }; }
componentDidCatch(error, errorInfo) { // Log to error reporting service reportError(error, errorInfo); }
render() { if (this.state.hasError) { return <ErrorFallback error={this.state.error} />; } return this.props.children; }}
// Usage: wrap any subtree<ErrorBoundary> <SomeFeature /></ErrorBoundary>;Without error boundaries, a thrown error in any component takes down the entire app. With boundaries, only the wrapped subtree shows the fallback. The rest of the app keeps working.
In production, I put error boundaries around:
- Each major page section (header, main content, sidebar)
- Third-party components (if their code throws, your app survives)
- Data-dependent components that might receive unexpected shapes
The react-error-boundary library wraps this in a function component API with retry support. Worth using instead of writing the class by hand.
Controlled vs uncontrolled
Section titled “Controlled vs uncontrolled”This pattern applies mostly to form inputs, but the concept is general.
Controlled: React state is the source of truth. The input displays what state says.
function Controlled() { const [value, setValue] = useState(""); return <input value={value} onChange={(e) => setValue(e.target.value)} />;}Every keystroke updates state, which re-renders the component, which updates the input. You have full control: you can validate, transform, or reject input on every change.
Uncontrolled: The DOM is the source of truth. React reads the value when it needs to.
function Uncontrolled() { const inputRef = useRef();
function handleSubmit() { console.log(inputRef.current.value); // read from DOM }
return <input ref={inputRef} defaultValue="" />;}No state updates on every keystroke. The input manages its own state internally. You just grab the value when you need it (usually on submit).
When to use which:
- Controlled when you need real-time validation, formatting (phone numbers, currency), or the value drives other UI
- Uncontrolled when you just need the value on submit and don’t want the re-render cost of controlled inputs (rare, but relevant for large forms)
- React Hook Form when you want the performance of uncontrolled with the validation power of controlled. It uses refs internally but gives you a controlled-feeling API.
Interview angles
Section titled “Interview angles”“What patterns do you use in production React?” Custom hooks for logic reuse, compound components for flexible UI libraries, error boundaries for resilience, composition over prop-heavy configuration. Mention that HOCs and render props are legacy patterns you’ve seen but wouldn’t write for new code.
“How do you handle errors in React?” Error boundaries at section boundaries. They catch render errors and show fallback UI without taking down the whole app. Combine with error reporting (Sentry, Datadog) in componentDidCatch. Note that error boundaries don’t catch errors in event handlers, async code, or server-side rendering. Those need regular try/catch.
“Controlled vs uncontrolled?” Controlled gives you per-keystroke control, uncontrolled gives you performance. For most forms, controlled is fine. For very large or performance-sensitive forms, React Hook Form gives you the best of both by using refs internally while exposing a clean validation API.