๐Ÿณ IntuitiveFE
Login
โ† All concepts

Render Props, HOC & Compound Components

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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

Legacy

Fill-in-the-blank form

Shares logic; consumer controls rendering. Replaced by custom hooks.

HOC

Legacy

Wearing someone else's jacket

Wraps component, injects props. Opaque, hard to debug. Replaced by hooks.

Compound Components

Current best

LEGO kit

Context shares state. Sub-components compose structure. Still the right pattern for UI libraries.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Render Props
ts
1// Render props โ€” share logic via a function prop
2interface MouseProps {
3 render: (pos: { x: number; y: number }) => React.ReactNode;
4}
5ย 
6function 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
24function 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}
Gotcha: Passing an inline function as render prop defeats React.memo โ€” the child receives a new function reference every render. Extract the render function or use useCallback.
Insight: Render props are still useful for rendering patterns where the consumer needs access to context that doesn't naturally fit a hook โ€” virtualization libraries (react-window), drag-and-drop (react-beautiful-dnd) still use them effectively.
Explored:๐ŸŽญ๐ŸŽฉ๐Ÿงฉ๐ŸŽ›๏ธ๐Ÿ”„
๐ŸŽฏ

Challenge

Explore all 5 advanced React patterns. For each, check the modern alternative tab.

Try it
๐ŸŽฏ

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)

js
1// asChild merges props onto the child element, no wrapper div
2import { Slot } from '@radix-ui/react-slot';
3ย 
4function 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.
1/4

When might a HOC still be preferable to a custom hook?