Render Props, HOC & Compound Components
โฑ๏ธ ~4-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.Each pattern is a different way to share capabilities between components. Render Props = handing someone a fill-in-the-blank form. HOC = wearing someone else's jacket. Compound Components = a LEGO set.
Render Props
LegacyFill-in-the-blank form
Shares logic; consumer controls rendering. Replaced by custom hooks.
HOC
LegacyWearing someone else's jacket
Wraps component, injects props. Opaque, hard to debug. Replaced by hooks.
Compound Components
Current bestLEGO kit
Context shares state. Sub-components compose structure. Still the right pattern for UI libraries.
Interactive Sandbox
โ Move something, see it react instantly.Pattern
| 1 | // Render props โ share logic via a function prop |
| 2 | interface MouseProps { |
| 3 | render: (pos: { x: number; y: number }) => React.ReactNode; |
| 4 | } |
| 5 | ย |
| 6 | function Mouse({ render }: MouseProps) { |
| 7 | const [pos, setPos] = useState({ x: 0, y: 0 }); |
| 8 | return ( |
| 9 | <div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}> |
| 10 | {render(pos)} {/* consumer controls the rendering */} |
| 11 | </div> |
| 12 | ); |
| 13 | } |
| 14 | ย |
| 15 | // Usage |
| 16 | <Mouse render={({ x, y }) => <Dot x={x} y={y} />} /> |
| 17 | ย |
| 18 | // Or using children as a function (same pattern) |
| 19 | <Mouse> |
| 20 | {({ x, y }) => <Dot x={x} y={y} />} |
| 21 | </Mouse> |
| 22 | ย |
| 23 | // Modern equivalent: custom hook |
| 24 | function useMouse() { |
| 25 | const [pos, setPos] = useState({ x: 0, y: 0 }); |
| 26 | const ref = useRef<HTMLDivElement>(null); |
| 27 | useEffect(() => { |
| 28 | const el = ref.current; |
| 29 | if (!el) return; |
| 30 | const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY }); |
| 31 | el.addEventListener('mousemove', handler); |
| 32 | return () => el.removeEventListener('mousemove', handler); |
| 33 | }, []); |
| 34 | return { pos, ref }; |
| 35 | } |
Challenge
Explore all 5 advanced React patterns. For each, check the modern alternative tab.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: Why did render props fall out of favor after hooks?
Render props solve a real problem (sharing stateful logic) but at the cost of JSX nesting. Every shared behavior requires a component wrapper that adds a layer of indirection. Custom hooks solve the same problem โ extract the logic into a `use*` function โ without any JSX nesting, wrapper components, or prop threading.
Q: What HOC problems do hooks solve?
HOCs inject props silently โ you can't easily see where a prop came from when multiple HOCs are composed. Hook calls are explicit in the component body: `const user = useAuth()` tells you exactly where `user` comes from. Hooks also avoid naming collisions between HOCs, and they don't add extra component layers to the React DevTools tree.
Q: When should you use the `as` prop?
Use the `as` prop when the rendered element's semantic type should be determined by context. A button that navigates should render as an anchor (`as="a"`). A text component that can be a heading or paragraph. When the element is always the same (e.g., a badge is always a `span`), hard-code it โ don't add polymorphism complexity unnecessarily.
The Deep Dive
โ Spec refs, engine internals, the minutiae.Headless UI libraries โ the pattern fully realized
Radix UI, Ark UI, and react-aria provide compound components for every interactive widget (Select, Dialog, Tooltip, Menu, Combobox) with complete ARIA implementation, keyboard navigation, and focus management. You bring the CSS; they bring the behavior. This is compound components at scale.
Slot pattern (Radix asChild)
| 1 | // asChild merges props onto the child element, no wrapper div |
| 2 | import { Slot } from '@radix-ui/react-slot'; |
| 3 | ย |
| 4 | function Button({ asChild, ...props }: ButtonProps) { |
| 5 | const Comp = asChild ? Slot : 'button'; |
| 6 | return <Comp className={styles.button} {...props} />; |
| 7 | } |
| 8 | ย |
| 9 | // Renders as <a> with button styles and event handlers |
| 10 | <Button asChild> |
| 11 | <a href="/next-page">Go</a> |
| 12 | </Button> |
| 13 | ย |
| 14 | // Result: <a href="/next-page" class="button-styles">Go</a> |
| 15 | // Not: <button class="..."><a href="...">Go</a></button> |
TypeScript polymorphic component typing
The `as` prop pattern requires careful TypeScript generics. The key types are `ElementType` (any HTML element or React component), `ComponentPropsWithoutRef<T>` (props for that element without ref), and an `Omit` to prevent the `as` prop from conflicting with itself. This is complex enough that many teams prefer Radix's `asChild` to avoid the generics entirely.
Interview Questions
โ Real questions from real interviews โ with answers.Custom hooks share the same logic without JSX nesting or wrapper component overhead.
HOCs inject props silently, cause wrapper hell in DevTools, and have naming collisions; hooks are explicit and visible.
Via React Context: the root component provides state; sub-components consume it without prop drilling.
Controlled when you need to validate or derive state from the value; uncontrolled when you only read it on submit.
The component accepts an 'as' prop that changes the rendered element type; TypeScript infers the correct props via generics.
asChild merges all props onto the child element's existing DOM node via a Slot โ no generics, no wrapper element.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.When might a HOC still be preferable to a custom hook?