skip to content

Render Props and Higher-Order Components

These are the pre-hooks logic-reuse patterns, and they still appear in real codebases and in interviews. Expect to compare them with custom hooks and to explain the wrapper-hell and ref-forwarding problems that motivated the move.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

6

Why did custom hooks replace render props and higher-order components as React's default way to reuse stateful logic? Name the concrete problems with the older patterns.

level: middleimportance: must knowfreq 60%

answer

  1. reuse of logic paid for in tree nodes
  2. where did this prop come from?
  3. two wrappers, one prop name
  4. pyramid of callbacks in JSX
  5. hooks add no node, you name the result

basics

~20 s

Both older patterns reuse logic by adding components: HOCs stack into deep wrapper chains with silent prop collisions and hidden prop origins, and render props nest into callback pyramids. A hook is a plain function call whose result you name yourself, adding nothing to the tree.

solid answer

~60 s

Both older patterns solved logic reuse by wrapping components, and wrapping has a fixed set of costs. Stacked HOCs produce wrapper hell — a debugging tree ten nodes deep before you reach anything that renders — and because they inject props invisibly, you cannot tell from a component's source where `user` came from, or notice when two wrappers inject the same name. Render props avoid the tree depth problem for one use but nest into a pyramid as soon as you need three of them, and you cannot easily feed one callback's value into the next provider. Custom hooks reuse the *logic* without the wrapper: they add no node to the tree, the caller names each returned value at the point of use so collisions become impossible, and composing several is just several lines in the component body, each free to use the previous one's result. What did not change is that hooks cannot render anything, so wrappers survive wherever the reusable unit must own markup or a place in the tree.

go deeper

for a junior

Be able to say that both old patterns share logic by wrapping components, while a custom hook is just a function you call inside your component, so the tree stays flat.

for a middle

Name the concrete costs — wrapper depth, injected props with no visible origin, silent name collisions, nested callbacks — and show how calling hooks in sequence removes each one.

for a senior

Show the limits too: hooks render nothing, so providers, boundaries and item renderers still need components, and repackaging logic does not change how often anything re-renders.

for a principal

Argue it as a codebase-wide contract question — invisible prop injection at module boundaries is unversioned coupling between teams, which is the real reason to prefer explicit call-site composition.

## The shared premise of the old patterns Before hooks, state and lifecycle only existed inside components. So any technique for sharing stateful logic had to route it through a component: either you wrapped the consumer in a component that had the state (a HOC), or you rendered a component that had the state and let it call you back (a render prop). Both are the same trick with different ergonomics, and both inherit the same structural cost — reuse of *logic* is paid for in *tree nodes*. ## Cost 1: wrapper hell and lost provenance A screen assembled from five cross-cutting concerns ends up as five nested wrappers around one component that renders markup. Three things get worse together: - **Debugging.** The DevTools tree and stack traces fill with intermediate nodes, and finding the component that owns a piece of state means scrolling through wrappers. - **Provenance.** Open the component's source and you see it uses `props.currentUser` — but nothing in that file says where it came from. You must find the export statement at the bottom, read the composition chain, and work backwards. - **Collisions.** Two wrappers that both inject `data` collide silently. Whichever spreads last wins; nothing warns. Typing catches some of this, but only if the wrappers are typed precisely, which most legacy ones are not. ## Cost 2: the callback pyramid Render props push the same cost into JSX shape rather than tree depth at the module level: ```jsx <Auth> {(user) => ( <Theme> {(theme) => ( <Data userId={user.id}> {(rows) => <Table rows={rows} theme={theme} />} </Data> )} </Theme> )} </Auth> ``` The indentation grows with each concern, the ordering is load-bearing, and every value you need at the bottom must survive being threaded through the middle. It reads badly, but the deeper problem is that the composition is expressed in markup, so ordinary programming moves — extracting a variable, an early return, a conditional — are awkward or impossible. ## What a hook changes structurally ```jsx function Screen() { const user = useAuth(); const theme = useTheme(); const rows = useData(user.id); return <Table rows={rows} theme={theme} />; } ``` Compare that against both patterns above and the improvements are concrete rather than aesthetic: - **No node is added.** Three concerns, zero extra components, an unchanged tree. - **Names are chosen by the caller.** Collisions cannot occur, because you write `const user = ...` yourself. Provenance is on the same screen as the usage. - **Composition is ordinary code.** `useData(user.id)` consumes the previous line's value directly. In the HOC version, feeding one wrapper's injected prop into another wrapper's configuration requires an extra component in between; in the render-prop version it requires nesting in exactly the right order. - **Refs and statics stop being special.** Everything a wrapper had to hoist, forward or re-label simply does not arise, because there is no wrapper. ## What hooks did not fix Being honest about the limits is what separates a rehearsed answer from an understood one. - A hook renders nothing. Anything that must *be* in the tree — a provider, a boundary, a measured wrapper element, a row renderer invoked once per row — still needs a component, and often a function prop. - Hooks have their own discipline: fixed call order, no calls inside conditions or callbacks. HOCs and render props had no such rule. - Hooks do not reduce re-renders. Both old patterns and hooks re-render the consumer when the shared state changes; that is a separate concern from how the logic is packaged. - Sharing logic across components that must not share *state* still requires care either way — a custom hook gives each caller its own state, exactly as a separate wrapper instance did. ## The interview frame Neither older pattern is deprecated; there is no API to deprecate, since both are just conventions built on props. What changed is the default. In a React 19 codebase, logic that computes goes in a hook, and a wrapper appears only when something must sit in the tree. You will still read HOCs constantly in older application code and in libraries, so the expected answer is not "HOCs are bad" but a precise account of which costs disappear, which remain, and where a wrapper is still the correct shape.

  • If hooks are better for logic reuse, why do component libraries still ship render props?
    Because a hook cannot render. When the library must own the markup or the subtree — a virtualized list deciding which rows exist, a measurement wrapper, a drag surface handing you live coordinates — the reusable unit has to be a component, and a function prop is how it hands control of the inner markup back to you. Many libraries ship both: a hook for the state, a component for the rendering.
  • Does converting a HOC to a hook reduce the number of renders?
    No. Both re-render the consumer when the shared state changes; packaging is orthogonal to update frequency. The win is structural — no extra nodes, no injected-prop collisions, clearer provenance. If you were expecting a performance improvement, measure first, because the usual outcome is that render counts are identical.
  • How would you catch two stacked HOCs injecting the same prop name today?
    Types are the practical answer: precisely typed wrappers make the collision a compile error rather than a silent overwrite. Beyond that it is convention — namespaced prop names, and a rule that a component takes at most one or two wrappers. The structural fix is to stop injecting props at all and call a hook, where the caller names each value.

saying these in an interview costs you the question

  • Says hooks are syntactic sugar for HOCs
  • Claims render props were removed from React
  • Thinks converting to hooks reduces re-renders
  • Believes HOCs are a deprecated React API
  • Says hooks can replace every wrapper, including providers and boundaries

context

open as a page

In React, what is the render-prop pattern — including the function-as-children form — and how does the callback get the data it renders?

level: juniorimportance: should knowfreq 50%

basics

~20 s

A render prop is a prop whose value is a function that returns JSX. The component holding the state calls that function during its own render and passes its data in as arguments, so the caller controls the markup while the component keeps the behaviour.

open as a page

What is a higher-order component in React — a `withX(Component)` wrapper — and what commonly breaks when one is written naively?

level: middleimportance: should knowfreq 55%

basics

~20 s

A higher-order component is a function that takes a component and returns a new component wrapping it. Naive versions swallow props, appear unnamed in DevTools, lose the inner component's static properties, and remount the whole subtree if the wrapper is created during render.

open as a page

In React 19, a `withTooltip(Button)` wrapper is rendered with a `ref` so the caller can focus the underlying `<button>`. Does the ref reach it, and how did this differ in React 18?

level: middleimportance: should knowfreq 40%

basics

~20 s

In React 19 the ref reaches the inner component, because ref is an ordinary prop on function components, so a wrapper that spreads its props forwards it automatically. In React 18 and earlier the ref was dropped unless the wrapper was built with forwardRef.

open as a page

In a React 19 codebase where custom hooks are the default, when is a render prop still the right API for a reusable component, and what does a hook not give you?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A render prop still wins when the reusable unit must own rendering or a place in the tree: item renderers for virtualization, wrapper elements that measure or capture input, and headless components that supply state and props but not markup. Hooks run inside the caller and render nothing.

open as a page

You lead a large React codebase where most screens are exported through chains of legacy `withX` higher-order components. How do you plan the move to hooks without a freeze-and-rewrite?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Move incrementally: implement each behaviour once as a hook, redefine the legacy wrapper as a thin shim over that hook so both call styles share one implementation, then convert call sites leaf-first while blocking new wrapper usage. Rank by debugging pain, and accept permanent residue.

open as a page