skip to content

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%

answer

  1. a function returning a new component
  2. composition at the module level
  3. spread the props you received
  4. name the wrapper for DevTools
  5. never build it during render

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.

solid answer

~50 s

A higher-order component is a plain function: it takes a component and returns a new component that renders the original with extra props or behaviour attached. It composes at the module level rather than in JSX, which is why it was the standard way to apply cross-cutting concerns before hooks. Four things break when it is written carelessly. It must spread the props it received onto the inner component, or callers silently lose props they passed. It should set a `displayName` such as `withLogger(Profile)`, otherwise the React DevTools tree fills with anonymous wrappers and stack traces get harder to read. Static properties defined on the inner component are not copied to the wrapper, so `Inner.someStatic` disappears. And calling the HOC inside a component's render creates a brand-new component type every render, so React unmounts and remounts the subtree and all its state is lost — the wrapper must be built once at module scope.

code

jsx · 22 lines
jsx
import { useEffect } from 'react';

function withLogger(Wrapped) {
  function WithLogger({ ...props }) {
    useEffect(() => {
      console.log('mounted:', WithLogger.displayName);
    }, []);
    return <Wrapped {...props} />;
  }

  WithLogger.displayName = `withLogger(${
    Wrapped.displayName || Wrapped.name || 'Component'
  })`;

  return WithLogger;
}

function Profile({ userId }) {
  return <div>user {userId}</div>;
}

export default withLogger(Profile);

go deeper

for a junior

Recognise the shape: a function named withSomething that takes a component and hands back another one, and know that the wrapper must pass the props it received through to the inner component.

for a middle

Explain the four concrete failure modes — dropped props, missing displayName, statics that do not travel, and the remount caused by building the wrapper during render — and why the last one happens.

for a senior

Show that you can audit an existing wrapper safely and decide whether the behaviour belongs around the component at all, versus inside it as a hook, before touching the call sites.

for a principal

Own the API consequence: a HOC injects props invisibly at the module boundary, so its contract is unversioned and its collisions are silent, which is a maintenance liability across many teams.

## What a HOC actually is There is no HOC API in React. A higher-order component is a naming convention for an ordinary function whose input is a component and whose output is a new component: ```jsx function withLogger(Wrapped) { function WithLogger(props) { useEffect(() => { console.log('mounted'); }, []); return <Wrapped {...props} />; } return WithLogger; } export default withLogger(Profile); ``` The interesting property is *where* the composition happens. A render prop composes inside JSX, at the point of use. A HOC composes at the module boundary: the module exports an already-enhanced component, and every call site gets the behaviour without knowing about it. That was a genuine advantage in the pre-hooks era for cross-cutting concerns — analytics, permission checks, theme injection — and it is also the root of most of the pattern's problems, because the enhancement is invisible at the call site. ## Hazard 1: swallowed props The wrapper receives everything the caller passed. If it destructures only the props it cares about and forgets the rest, those props never reach the inner component: ```jsx function withTheme(Wrapped) { return function WithTheme({ theme }) { return <Wrapped theme={resolve(theme)} />; // every other prop is dropped }; } ``` The fix is the rest pattern plus a spread: `function WithTheme({ theme, ...rest }) { return <Wrapped {...rest} theme={resolve(theme)} />; }`. Order matters — props spread later win, so decide deliberately whether the caller or the HOC gets the last word on a colliding name. ## Hazard 2: no displayName React derives a component's name in DevTools from the function's name, and a HOC that returns an anonymous arrow gives you a tree full of indistinguishable entries. Set it explicitly, and include the inner name so the tree stays readable: ```jsx WithLogger.displayName = `withLogger(${Wrapped.displayName || Wrapped.name || 'Component'})`; ``` This matters more than it sounds. The main practical complaint about HOC-heavy codebases is that debugging means scrolling past ten wrapper nodes to find the component that actually renders something; unnamed wrappers make that worse. ## Hazard 3: statics are not inherited The returned component is a *new* function. Anything attached to the original — a `defaultProps`-style constant, a `Component.Item` sub-component used in a compound API, a query definition, a `propTypes` object — lives on the original and is simply absent from the wrapper. Callers who relied on `Profile.Header` now get `undefined`. Either copy the statics you know about explicitly, or use the well-known `hoist-non-react-statics` package, which copies everything except the properties React itself owns. ## Hazard 4: creating the wrapper during render This is the bug that costs an afternoon: ```jsx function Page(props) { const Enhanced = withLogger(Profile); // new function identity every render return <Enhanced {...props} />; } ``` React decides whether to keep or discard a subtree by comparing element *types*. `withLogger(Profile)` returns a different function object on every render of `Page`, so the type never matches the previous one, and React tears the whole subtree down and builds it again. Every piece of state inside it resets, effects re-run their cleanup and setup, inputs lose focus and scroll position jumps. The rule is simple: apply HOCs once, at module scope, never inside a component body or a callback. ## Cost model, and where HOCs sit today Even a perfectly written HOC adds a node to the tree, hides the origin of the props it injects, and offers no static protection against two stacked HOCs injecting the same prop name. Those costs are exactly what pushed React towards hooks: a hook adds no node, and its results are named locally by the code that destructures them. So in a React 19 codebase you write HOCs rarely — mostly when the behaviour genuinely has to sit *around* a component rather than inside it — but you read them constantly, because a decade of libraries and internal utilities are built on them. The interviewer is checking that you can pick one apart safely: does it forward everything, is it named, does it lose statics, and is it applied at module scope.

  • Two stacked HOCs both inject a prop called `data`. What happens, and how would you have prevented it?
    The one applied closest to the inner component wins or loses depending on spread order, and nothing warns you — the collision is silent because neither wrapper knows the other exists. Prevention is by convention (namespaced prop names) or by typing the wrapper so the compiler flags the clash. It is one of the structural arguments for hooks, where you name each result yourself at the call site.
  • Why can a HOC not be replaced by simply calling a custom hook inside the wrapped component?
    Often it can, and that is the usual modern refactor. It cannot when the behaviour must exist around the component rather than inside it — rendering a provider or a boundary above it, deciding not to render it at all, or attaching behaviour to a third-party component whose source you do not control. Those are the cases where a wrapper still earns its node in the tree.

saying these in an interview costs you the question

  • Thinks HOC is a React API you import
  • Forgets to spread the remaining props onto the inner component
  • Assumes statics and sub-components come along automatically
  • Creates the wrapper inside render and blames React for lost state
  • Says a HOC mutates the component it is given

context