Skip to content

React · Patterns

React Render Props & HOCs — Complete Interview Guide

8 min read Updated 2026-06-24 Share:

Practice Render Props & HOCs interview questions

The Logic-Reuse Problem Before Hooks

Before React 16.8 introduced hooks, sharing stateful logic between components was genuinely hard. You had three options: copy-paste the logic, lift it into a common ancestor (coupling unrelated components), or use one of two composition patterns — render props or Higher-Order Components (HOCs). Both patterns solved the problem. Both left a legacy in every React codebase written before 2019, and interviewers still ask about them because they appear in existing code and because understanding them deepens your grasp of React's component model.

Render Props

A render prop is a prop whose value is a function. The component that owns the state calls that function instead of rendering JSX directly, passing the state as arguments. The caller decides what to render.

function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url).then(r => r.json()).then(d => { setData(d); setLoading(false); });
  }, [url]);

  return render({ data, loading }); // state flows out, rendering is delegated
}

<DataFetcher
  url="/api/users"
  render={({ data, loading }) =>
    loading ? <Spinner /> : <UserList users={data} />
  }
/>

Function-as-Child

When the render prop is named children instead of render, the pattern is called function-as-child (or "children as a function"). Behaviour is identical — the component calls props.children(state). The syntax difference is cosmetic: the function lives inside the JSX tag body, which can read more naturally.

<Mouse>
  {({ x, y }) => <Cursor x={x} y={y} />}
</Mouse>

Use an explicit render prop when you need multiple render slots (renderHeader, renderFooter). Use children when there is only one slot.

Higher-Order Components

A Higher-Order Component is a function that takes a component and returns a new, enhanced component — the React adaptation of the higher-order function concept. The canonical naming convention is withXxx (camelCase, "with" prefix).

function withAuth<P extends WithAuthProps>(
  WrappedComponent: React.ComponentType<P>
): React.ComponentType<Omit<P, keyof WithAuthProps>> {

  function WithAuth(props: Omit<P, keyof WithAuthProps>) {
    const { user } = useAuth();
    if (!user) return <Navigate to="/login" replace />;
    return <WrappedComponent {...(props as P)} currentUser={user} />;
  }

  WithAuth.displayName =
    `WithAuth(${WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'})`;

  return WithAuth;
}

Always set displayName in the WithXxx(InnerName) format. Without it, React DevTools shows anonymous components and stack traces become unreadable.

Cross-Cutting Concerns

HOCs excel at cross-cutting concerns — behaviour that applies uniformly across many components without those components needing to know about it:

  • Auth guards — redirect unauthenticated users before rendering a page
  • Analytics logging — record component mount/unmount without touching business logic
  • Error boundaries — wrap any component with standardised error UI

The consumer's code stays clean; the HOC handles the concern invisibly.

Prop Collision

HOC-injected props can silently overwrite props that parents or consumers supply with the same name. The fix is using namespaced prop names and making the contract explicit in TypeScript:

// BAD — HOC injects "user" which could clash with a parent's "user" prop
function withUser(Wrapped) {
  return props => <Wrapped {...props} user={getCurrentUser()} />;
}

// GOOD — descriptive namespaced name avoids collisions
function withUser(Wrapped) {
  return props => <Wrapped {...props} currentUser={getCurrentUser()} />;
}

HOC Composition

Stacking multiple HOCs with nested calls reads inside-out and becomes unmaintainable. A compose utility applies functions right-to-left, making the reading order match the wrapping order:

import { compose } from 'redux'; // or any compose utility

const enhance = compose(
  withLogger,   // applied last (outermost)
  withAuth,     // applied second
  withTheme,    // applied first (innermost)
);

export default enhance(MyComponent);

Order matters: the outermost HOC receives props first. If withAuth reads a theme prop provided by withTheme, then withTheme must be listed after withAuth in the compose call.

Wrapper Hell

In React DevTools, heavily HOC-wrapped components show as nested layers: WithRouter > WithTheme > WithAuth > WithLogger > Dashboard. This "wrapper hell" makes the component tree hard to navigate and error stacks hard to read. The solution is collapsing HOC stacks into custom hooks — which became the standard approach after React 16.8.

Why Hooks Replaced Them

Custom hooks solve the same logic-reuse problem with none of the drawbacks:

IssueRender props / HOCsCustom hooks
Extra DOM nodesYes — each adds a wrapper componentNo — hooks run inside the calling component
Prop collisionHOCs inject into the prop namespaceHooks return values via destructuring
DevTools clarityDeep wrapper treeFlat component tree
TypeScriptGenerics + conditional typesPlain function return types
// Pre-hooks: render prop for mouse position
<MouseTracker render={({ x, y }) => <Crosshair x={x} y={y} />} />

// Post-hooks: custom hook — same logic, no wrapper component
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handler = e => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
  return pos;
}

When They're Still the Right Tool

Despite hooks, render props and HOCs are still relevant in specific cases:

  • Render delegation to a libraryreact-window's <FixedSizeList> calls a render prop for each row because the library controls the timing and injected style. Hooks cannot replace this.
  • Class component consumers — hooks cannot run inside class components. An HOC is the bridge that feeds hook-based logic into a class component.
  • Transparent interception — HOCs can decorate a component without the consumer noticing, which is the right model for analytics or logging.

Static Methods and forwardRef

Two common pitfalls with HOCs that interviewers probe:

Static methods are lost. The HOC returns a new function object; static methods from the original (like Next.js getLayout) do not copy over automatically. Fix: hoistNonReactStatics(WithAuth, WrappedComponent).

Refs do not forward. ref is not a real prop — it attaches to the wrapper, not the wrapped component. Fix: wrap the HOC body in React.forwardRef and thread the ref argument through:

const WithFocusRing = React.forwardRef(function(props, ref) {
  return <WrappedComponent {...props} ref={ref} />;
});
hoistNonReactStatics(WithFocusRing, WrappedComponent);

Performance: The Inline Render Prop Trap

Passing an inline arrow function as a render prop creates a new function reference on every parent render. When the render-prop component uses React.memo or PureComponent, the shallow comparison always fails and the optimisation is bypassed.

Fix: stabilise the reference with useCallback (functional components) or a class instance method (class components):

// BAD — new reference every render, memo is defeated
<Mouse render={({ x, y }) => <Crosshair x={x} y={y} />} />

// GOOD — stable reference
const renderCrosshair = useCallback(({ x, y }) => <Crosshair x={x} y={y} />, []);
<Mouse render={renderCrosshair} />

Testing Strategy

For HOC-wrapped components, use the dual export pattern: export the base component as a named export and the HOC-wrapped version as the default. Unit tests import the named export to test business logic in isolation; integration tests import the default to test the HOC's own behaviour.

export function UserList({ currentUser }) { /* ... */ }   // named — no HOC
export default withAuth(UserList);                         // default — wrapped

// Unit test — bypasses HOC
import { UserList } from './UserList';
render(<UserList currentUser={mockUser} />);

Key Takeaways

  • A render prop delegates rendering by calling a function prop with shared state — the caller decides the UI.
  • Function-as-child is a render prop where the prop name is children; the mechanics are identical.
  • An HOC is a function from component to component; name it withXxx and always set displayName.
  • HOCs are ideal for cross-cutting concerns (auth guards, logging) that should be invisible to consumers.
  • Prop collision is avoided with namespaced injection prop names and TypeScript Omit<P, keyof InjectedProps>.
  • Compose HOCs with a compose utility to avoid inside-out nesting.
  • Custom hooks replaced both patterns for most new code — but render props are still the right API when a library needs a JSX factory, and HOCs remain the only bridge into class components.
  • Always use hoistNonReactStatics and React.forwardRef together in every HOC.
  • Stabilise render prop function references with useCallback to avoid defeating memoisation.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel