skip to content

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%

answer

  1. does it compute, or does it render?
  2. a hook has no place in the tree
  3. one call per visible row
  4. values that exist only inside the subtree
  5. ship the hook, wrap it in the component

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.

solid answer

~60 s

The dividing line is whether the reusable thing has to exist in the tree. A hook runs inside the caller's own component and produces values; it cannot render an element, cannot wrap a subtree, cannot decide not to render its consumer, and cannot run once per item. So a render prop is still the right shape when the component owns the DOM structure — a virtualized or windowed list that calls your renderer for each visible index, a wrapper that measures its own element or captures pointer input and hands you live values, a drag surface. It is also right when the values only exist *inside* that subtree, so the caller must be positioned there to see them. And it is the natural API for headless components that own behaviour and accessibility wiring while leaving markup entirely to you. The costs you accept are an extra node, a callback recreated each render, and code that reads more heavily nested — worth it only when the alternative would force the consumer to reimplement the rendering.

go deeper

for a junior

Remember the simple split: a hook gives you values to use in your own JSX, while a component with a function prop is what you reach for when the component itself has to render something around or instead of yours.

for a middle

Explain that the callback runs inside the owner's render, which is what lets a list invoke it once per visible row and what forbids calling hooks inside it.

for a senior

Make the call on a real API: identify whether the reusable unit computes or renders, name the concrete cases that force a component, and state the costs you are accepting when you choose one.

for a principal

Own it as a library contract: a function prop hands consumers control of markup permanently, so its argument shape becomes public API, and shipping both a hook and a component means committing to keep them consistent.

## The one thing a hook cannot do A custom hook is a function that runs inside a component. It can hold state, subscribe, compute and return values. It cannot occupy a position in the tree. That single limitation generates the whole list of cases where a function prop is still correct. Ask of any reusable unit: does it need to *render*, or only to *compute*? If it only computes, extract a hook and stop. If it must render — its own element, your elements, several copies of your elements, or nothing at all — then a component is required, and once a component must render caller-supplied markup, a function prop is how the caller gets that control. ## Case 1: it renders your markup N times A virtualized list only renders the rows currently in view, and only it knows which indexes those are. It therefore must call your renderer once per visible index: ```jsx <VirtualList count={rows.length} itemHeight={32}> {(index) => <Row data={rows[index]} />} </VirtualList> ``` No hook can express this. A hook could hand you the visible range and let you map over it yourself, and some libraries do offer exactly that — but then the consumer must reimplement the scroll container, the spacer elements and the positioning, which is most of the value. ## Case 2: the values only exist inside the subtree Some data is a property of a position in the tree rather than of the application: the measured size of a specific element, the pointer position relative to a drop zone, whether *this* subtree is currently within a transition. A wrapper component can render an element, observe it, and hand the result to a function that renders inside it. A hook called by the parent has no element to observe yet — it would have to be given a ref, which pushes the ownership of the element back to the consumer, which is often precisely what you were trying to encapsulate. ## Case 3: it decides whether your markup renders at all A gate — a permission check, a feature flag, a boundary that swaps in a fallback — must be able to return something *other* than your content. That is a rendering decision, so it belongs to a component. A hook can only return a boolean and leave the branching to every caller, which is fine for one call site and a duplication problem across fifty. ## Case 4: headless components A headless component owns behaviour, keyboard interaction and accessibility attributes, and owns none of the visual markup. It hands the caller state plus objects of props to spread onto elements. This is a deliberate API decision rather than a technical necessity — much of it could be a hook, and many libraries do ship the hook as the primary API with a component as convenience. The component form wins where the correct DOM relationships (which element carries which role, what nests inside what) are part of what is being encapsulated. ## Case 5: what still needs a class One narrow case is not a matter of taste: catching render errors requires a class component, so an error boundary is always a component in the tree, never a hook. Anything you want to apply *around* a subtree in that way is a wrapper by construction. ## The costs you are accepting Be explicit about them, because an interviewer will probe: - One extra node in the tree per usage, and the debugging weight that comes with it. - The callback is a fresh function on every render of the caller, so the receiving component cannot skip work by comparing that prop; if the callback does expensive work, it does it again. - Nesting grows if several such components are combined, and ordering becomes load-bearing. - The callback runs inside the owner's render, so it cannot call hooks — any state the caller needs must be lifted above the wrapper. ## The decision rule One sentence, and it survives the follow-ups: **extract a hook when the reusable part computes, a component with a function prop when the reusable part renders.** Where both are genuinely useful, ship both — the hook as the primitive and the component as the convenience layer over it — rather than forcing every consumer through the heavier shape.

  • You are designing a reusable component and both a hook and a render prop seem workable. How do you decide what to ship?
    Ship the hook as the primitive, because it composes with ordinary code and adds nothing to the tree, then build the component on top of it for consumers who want the rendering handled. That keeps one implementation and two entry points. Ship only the component when the DOM structure or the element relationships are themselves part of the contract you are encapsulating.
  • What is the cost of an inline arrow as a render prop, and when does it actually matter?
    The callback is a new function on every render of the caller, so the receiving component sees a changed prop and cannot bail out of re-rendering. It matters when that component's render is expensive — a long list, a chart — and is invisible otherwise. Measure before restructuring; the usual fix is to move state down so fewer renders reach the wrapper at all.

saying these in an interview costs you the question

  • Says every render prop can be replaced by a hook
  • Thinks a hook can render a wrapper element around the caller
  • Claims an error boundary can be written as a hook
  • Chooses render props for logic that produces no markup
  • Ignores that the callback cannot call hooks

context