What Are Render Props and How Are They Different from HOCs?

Advanced10 min interview
Skills tested:
Explaining how render props invert rendering control to the consumerExplaining how HOCs operate at the component definition level instead of JSX levelIdentifying the silent prop collision failure mode in HOCsKnowing that inline render prop functions create new references per renderArticulating why hooks supersede both patterns and when each remains appropriate

Advertisement

🧩 Scenario

Render props and HOCs appear in senior interviews as legacy pattern knowledge and as a setup for explaining why hooks superseded them. Interviewers want the structural reason, not just "hooks are better."

Architecture Walkthrough

Why Render Props Invert Rendering Control

The render prop pattern works by delegating the rendering decision entirely to the consumer. The component that holds the stateful logic calls props.render(state) instead of deciding on its own what to display. This inversion is the entire point: the same MouseTracker component can render a crosshair, a tooltip, or a custom cursor depending on who is using it, without any modification to MouseTracker itself. The consumer passes a function that receives the tracked state and returns JSX. The rendering logic lives at the call site, where it belongs.

Without this pattern, you would need to accept explicit configuration props for every possible rendering variation. A render prop compresses infinite variations into a single function call. Any prop whose value is a function is a render prop — the prop name can be render, children, or anything else.

Why HOCs Compose at the Component Level

A HOC operates at a different layer of abstraction. Instead of controlling what renders inside a function call, it wraps the entire component definition and produces a new one. The original component remains completely unaware of the enhancement. This makes HOCs ideal for cross-cutting concerns: authentication guards, analytics tracking, permission checks. The enhancement is applied once at module level, not repeated in every JSX tree where the component is used.

The tradeoff is that the caller loses rendering control. The HOC decides what to show during loading, error, or unauthorized states, and the wrapped component cannot override that decision. This rigidity is exactly what makes HOCs predictable for auth guards but inflexible for rendering-variant scenarios.

Prop Collisions, Nesting, and Why Hooks Win

Both patterns have structural failure modes. With HOCs, silent prop collision: if withAuth injects user and a parent also passes user, one silently overwrites the other based on spread order. There is no runtime warning. With render props, nesting: each render prop component adds one level of indentation, so composing three produces a structure that reads inside-out with the actual JSX buried three callbacks deep.

Custom hooks achieve horizontal composition: each hook call is one line, hooks cannot collide on prop names because they do not inject props, and the rendering logic stays in the component where it is visible. The structural reason hooks supersede both patterns is that they compose horizontally while render props and HOCs compose vertically.


Key Code Explained

// Render prop: the consumer controls what renders
function MouseTracker({ render }: { render: (pos: Position) => React.ReactNode }) {
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });

  return (
    <div
      onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}
      style={{ height: '300px' }}
    >
      {render(position)}
    </div>
  );
}

// Consumer controls rendering for all states
<MouseTracker render={({ x, y }) => <Cursor x={x} y={y} />} />
<MouseTracker render={({ x, y }) => <Tooltip x={x} y={y}>Hover info</Tooltip>} />


// children-as-function: identical pattern, different prop name
function DataFetcher({
  url,
  children,
}: {
  url: string;
  children: (state: FetchState) => React.ReactNode;
}) {
  const { data, isLoading, error } = useFetch(url);
  return <>{children({ data, isLoading, error })}</>;
}

// Call site: consumer handles all three states differently per context
<DataFetcher url="/api/orders">
  {({ data, isLoading, error }) =>
    isLoading ? <Skeleton /> : error ? <ErrorBanner /> : <OrderTable rows={data} />
  }
</DataFetcher>


// HOC: wraps component definition, injects behavior via props
function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
  function AuthGuard(props: P) {
    const { user, isLoading } = useAuth();

    if (isLoading) return <Spinner />;
    if (!user) return <Navigate to="/login" replace />;

    return <WrappedComponent {...props} />;
  }

  AuthGuard.displayName = `withAuth(${
    WrappedComponent.displayName ?? WrappedComponent.name
  })`;

  return AuthGuard;
}

const ProtectedDashboard = withAuth(Dashboard);  // applied at module level


// Custom hook: horizontal composition — no nesting, no prop collision
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handler = (e: MouseEvent) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);

  return position;
}

// Component decides what to render with the shared state — no nesting
function CustomCursor() {
  const { x, y } = useMousePosition();
  return <div style={{ left: x, top: y }} className="cursor" />;
}

The children render prop in DataFetcher shows why naming matters for readability. The consumer explicitly handles all three states at the call site — a HOC version would make all three state UIs static and defined inside the HOC, which would look different on every page where the fetcher is used.


Tradeoffs

PatternConsumer rendering controlProp collision riskComposition depthStill used in
Render propFullNoneNests with each compositionFormik v1, React Router v5, D3 wrappers
HOCNone (HOC controls it)Yes (silent)Flat (module-level)Auth guards, legacy codebases, library adapters
Custom hookFullNoneFlat (one line per hook)Modern React (2019+)

What Interviewers Actually Check

  • Whether you know "render prop" means any function-as-prop, not just props named render
  • Whether you identify the silent prop collision failure mode in HOCs
  • Whether you know that inline render prop functions create new references per render
  • Whether you articulate the structural reason hooks supersede both patterns (horizontal vs vertical composition)
  • Whether you know when a HOC remains the right choice over a hook (intercepting rendering to redirect before render)

Follow-Up Questions

  1. If you have a render-prop <Toggle> component and want to convert it to useToggle, what exactly changes at the call site and what stays the same?
  2. Two HOCs both inject a prop named user. The app works in dev but shows wrong data on one page. How do you diagnose and fix this?
  3. A render prop component re-renders 60 times per second tracking mouse position. The child component is expensive. How do you prevent the child from re-rendering on every tick without changing the render prop component?
  4. React DevTools shows Unknown for a component wrapped in three HOCs. How do you restore readable names?
  5. When would a HOC still be the correct choice over a custom hook in a modern React codebase?

Common Candidate Mistakes

  • Thinking render props must use a prop named render — any function-as-prop is the pattern
  • Not knowing that inline arrow functions as render props defeat React.memo on the child (new reference per render)
  • Not knowing HOC prop collisions are silent and can be detected only by auditing injected prop names
  • Saying "hooks replaced both patterns" without explaining the structural reason (horizontal vs vertical composition)
  • Not knowing that HOCs still make sense when you need to intercept rendering to prevent a component from mounting at all

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you implement a render prop component and a HOC for the same logic and explain the call site difference?
  • Can you explain the structural reason hooks supersede both patterns?
  • Can you explain when a HOC remains the correct tool?
  • Can you identify the prop collision failure mode and the nesting failure mode?
  • Can you explain why inline arrow functions as render props defeat React.memo?

Summary

Render props invert rendering control by passing shared state through a function call: the consumer decides what to render for all states. HOCs operate at the component definition level, injecting behavior as props on the wrapped component and controlling rendering themselves. Both patterns compose vertically: render props nest with each composition level, HOCs build up wrapper stacks. Custom hooks compose horizontally: each hook is one line with no JSX nesting and no prop injection. This structural difference is why hooks superseded both patterns for logic sharing. HOCs retain a use case when you need to intercept and conditionally prevent rendering (auth guards, feature flags) because custom hooks cannot execute a redirect before a component renders. Both patterns still appear extensively in codebases written before 2019 and in library APIs.

Frequently Asked Questions

Can I use children as a render prop?

Yes. Any prop whose value is a function is a render prop — the name "render" is convention, not a requirement. Using children as a function is identical to using a named render prop and is idiomatic in many libraries including React Router v5 and Formik.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement