Skip to content

Render Props & HOCs Interview Questions & Answers

19 questions Updated 2026-06-24 Share:

React render props and HOC interview questions — function-as-child, higher-order components, withAuth, cross-cutting concerns, hooks vs HOCs comparison.

Read the in-depth guideReact Render Props & HOCs — Complete Interview Guide(opens in new tab)
19 of 19

The render props pattern is a technique for sharing stateful logic between components by passing a function as a prop. The host component manages state and calls that function with the state as arguments, letting the consumer decide what to render.

The problem it solves is logic reuse without inheritance. Before hooks, if two components needed the same piece of state (mouse position, window size, data fetching status) you had three options: copy-paste the logic, lift state up (coupling unrelated components), or use render props / HOCs. Render props kept the logic encapsulated in one place while giving the consumer full rendering control.

// DataFetcher owns fetching logic; caller decides the UI
function DataFetcher({ url, render }) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

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

  // Delegates rendering entirely to the caller
  return render({ data, loading });
}

// Usage — caller controls the UI
<DataFetcher
  url="/api/users"
  render={({ data, loading }) =>
    loading ? <Spinner /> : <UserList users={data} />
  }
/>

Rule of thumb: Render props shine when you want to share complex stateful behaviour while letting each consumer render something different.

Function-as-child (also called "children as a function") is just a render prop where the prop name is children instead of a custom name like render. The component calls props.children(...) exactly as it would call props.render(...).

The only differences are:

  • Syntactic: the function lives inside the JSX tag body, which reads more naturally for simple cases.
  • Discoverability: explicit render props (render, renderHeader, etc.) are visible in prop-types/TypeScript signatures; children can surprise readers if they expect a ReactNode.
// Explicit render prop
<Mouse render={({ x, y }) => <Cursor x={x} y={y} />} />

// Function-as-child — identical behaviour, different syntax
<Mouse>
  {({ x, y }) => <Cursor x={x} y={y} />}
</Mouse>

// Inside Mouse — both approaches call the same way
function Mouse({ children }) {
  const [pos, setPos] = React.useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}>
      {children(pos)}  {/* or props.render(pos) */}
    </div>
  );
}

Rule of thumb: Use an explicit render prop when you need multiple render slots (renderHeader, renderFooter); use children when there is only one slot and the JSX nesting reads naturally.

A Higher-Order Component is a function that takes a component and returns a new, enhanced component. It is the React adaptation of the higher-order function concept — just as Array.map takes a function and returns a new array, an HOC takes a component and returns a component.

The canonical signature is:

// Generic signature: WrappedComponent in, EnhancedComponent out
function withSomething<P extends object>(
  WrappedComponent: React.ComponentType<P>
): React.ComponentType<P & InjectedProps> {

  function WithSomething(props: P) {
    const injected = useSomethingLogic(); // HOC-owned logic
    return <WrappedComponent {...props} {...injected} />;
  }

  // Naming convention: prefix with "with"
  WithSomething.displayName =
    `WithSomething(${WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component'})`;

  return WithSomething;
}

Key points:

  • The naming convention is withXxx (camelCase, "with" prefix) — e.g., withAuth, withTheme, withLogger.
  • Set displayName so React DevTools shows WithAuth(Dashboard) rather than an anonymous component.
  • The HOC does not mutate the wrapped component; it wraps it.

Rule of thumb: If a function takes a component and returns a component, it is an HOC; name it withXxx and set displayName.

You can nest HOC calls directly, but deep nesting reads inside-out and is hard to maintain. The standard solution is a compose utility (available in lodash, Redux, Ramda, or trivial to write) that applies functions right-to-left so the reading order matches the wrapping order.

// Naive nesting — reads inside-out, breaks easily when reordering
export default withLogger(withAuth(withTheme(MyComponent)));

// compose applies right-to-left: theme → auth → logger (outermost last)
import { compose } from 'redux'; // or lodash/fp

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

export default enhance(MyComponent);

// Minimal compose implementation for reference
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

Order matters: the outermost HOC's props arrive first. If withAuth reads a theme prop injected by withTheme, then withTheme must be applied before withAuth (listed after it in compose).

Rule of thumb: Use compose when stacking three or more HOCs; keep the list in inside-out (most-specific-first) order so execution matches your mental model.

Classic cross-cutting concerns suited to HOCs:

  1. Authentication / authorisation guards — redirect unauthenticated users before rendering a page component.
  2. Analytics / logging — record component mount, unmount, and prop changes without touching business logic components.
  3. Error boundaries — wrap any component with standardised error UI.

withAuth example in detail:

// withAuth.jsx — auth guard HOC
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

function withAuth(WrappedComponent, requiredRole = null) {
  function WithAuth(props) {
    const { user, loading } = useAuth();

    if (loading) return <Spinner />;
    if (!user) return <Navigate to="/login" replace />;
    if (requiredRole && user.role !== requiredRole)
      return <Navigate to="/forbidden" replace />;

    // User is authenticated (and has the right role) — render component
    return <WrappedComponent {...props} />;
  }

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

// Usage
export default withAuth(AdminDashboard, 'admin');

Rule of thumb: HOCs are ideal for cross-cutting concerns that need to intercept rendering (guards, error boundaries) or observe lifecycle events (logging) without altering the wrapped component's own logic.

Prop collision occurs when an HOC injects a prop whose name clashes with a prop the wrapped component (or a parent) already uses. The HOC's value silently wins because it is spread last (or first), overwriting the intended value and causing subtle bugs.

Prevention strategies:

// BAD — HOC spreads injected props after ownProps; "user" collision
function withUser(WrappedComponent) {
  return function(props) {
    const user = useCurrentUser();
    // If parent passes user={guestUser}, HOC's user overwrites it silently
    return <WrappedComponent {...props} user={user} />;
  };
}

// GOOD — namespace injected props under a dedicated key
function withUser(WrappedComponent) {
  return function({ injectedUser, ...ownProps }) {
    const user = useCurrentUser();
    // Injected prop has a unique name; no risk of shadowing parent props
    return <WrappedComponent {...ownProps} currentUser={user} />;
  };
}

// BEST — use TypeScript to make the contract explicit
type WithUserProps = { currentUser: User };
function withUser<P extends WithUserProps>(
  WrappedComponent: React.ComponentType<P>
) { /* ... */ }

Other strategies: document all injected prop names, prefix them (_injectedUser), or prefer hooks (which do not pollute the prop namespace at all).

Rule of thumb: Never spread HOC-injected props with generic names; use descriptive, namespaced names and make them explicit in TypeScript types.

Custom hooks solve the same logic-reuse problem without the downsides of both patterns:

Issue Render props / HOCs Custom hooks
Extra DOM nodes Yes — every HOC / render-prop host adds a wrapper in the tree No — hooks run inside the calling component
Prop drilling / collision HOCs inject props that can clash Hooks return values via destructuring — no prop namespace
Composability HOCs compose left-to-right but reading order is confusing Just call multiple hooks sequentially
TypeScript Generics + conditional types needed for good inference Plain function return types — straightforward
DevTools clarity Deep "wrapper hell" in component tree Flat component tree
// Pre-hooks: render prop for mouse position
<MouseTracker render={({ x, y }) => <Crosshair x={x} y={y} />} />

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

function Crosshair() {
  const { x, y } = useMousePosition(); // clean, no wrapper
  return <div style={{ top: y, left: x }} />;
}

Rule of thumb: Reach for a custom hook first; only use render props or HOCs when you need to control rendering structure (render prop) or integrate with class components or third-party code that cannot use hooks (HOC).

Render props remain the best choice in several scenarios:

  1. Controlled render delegation — when the parent needs to decide what renders inside a complex host (e.g., a virtualised list that injects row index/style and expects the caller to return the row JSX).
  2. Class component consumers — hooks cannot run inside class components; an HOC or render-prop component is the only way to feed hook-based logic into them.
  3. Library APIsreact-router (<Route render> in v5), formik (<Field>), and react-window (<FixedSizeList>) expose render props because they cannot know your render tree ahead of time.
// react-window — render prop is the correct API; hooks cannot replace it here
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    // style MUST be applied for virtualisation to work
    <div style={style}>{items[index].name}</div>
  );

  return (
    <FixedSizeList height={400} itemCount={items.length} itemSize={35} width="100%">
      {Row}
    </FixedSizeList>
  );
}

Rule of thumb: Render props are still the right tool when a library needs you to supply a JSX factory, or when the component consuming your logic is a class component.

A wrapped component's static methods (e.g., getLayout, fetchData, propTypes) are defined on the original component object. The HOC returns a new function object that does not automatically copy those statics, so callers that look up EnhancedComponent.getLayout find undefined.

The fix is hoist-non-react-statics, a small utility that copies all non-React statics from the wrapped component to the wrapper:

import hoistNonReactStatics from 'hoist-non-react-statics';

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

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

  // Copy getLayout, fetchData, propTypes, etc. from original
  hoistNonReactStatics(WithLogger, WrappedComponent);

  return WithLogger;
}

// Statics survive the wrapping
WithLogger.getLayout === WrappedComponent.getLayout; // true

React-specific statics (defaultProps, propTypes, contextType, displayName, getDerivedStateFromProps, etc.) are intentionally not hoisted — the library has a hardcoded exclusion list for them.

Rule of thumb: Always call hoistNonReactStatics(Wrapper, WrappedComponent) inside every HOC factory so page-level statics (especially Next.js getLayout) are not silently dropped.

HOCs break ref forwarding by default because ref is not a real prop — it is handled by React's reconciler and does not appear in props. A ref attached to the HOC wrapper points at the wrapper, not the underlying DOM node or component.

The fix is React.forwardRef:

function withFocusRing(WrappedComponent) {
  // forwardRef creates a component that passes its ref down
  const WithFocusRing = React.forwardRef(function(props, ref) {
    const [focused, setFocused] = React.useState(false);
    return (
      <div className={focused ? 'ring' : ''}>
        <WrappedComponent
          {...props}
          ref={ref}           // forward the ref to the real component
          onFocus={() => setFocused(true)}
          onBlur={() => setFocused(false)}
        />
      </div>
    );
  });

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

  hoistNonReactStatics(WithFocusRing, WrappedComponent);
  return WithFocusRing;
}

// Caller gets a ref to the real input, not the wrapper div
const EnhancedInput = withFocusRing(React.forwardRef((props, ref) =>
  <input ref={ref} {...props} />
));
const ref = React.createRef();
<EnhancedInput ref={ref} />;

Rule of thumb: Wrap the inner component in React.forwardRef and thread the ref argument through to WrappedComponent; always pair this with hoistNonReactStatics.

Both patterns can introduce unnecessary re-renders if not handled carefully.

HOC issues:

  • Each HOC in a composition chain is a separate React component — React.memo on the inner component does not prevent the wrapper from re-rendering.
  • If the HOC creates a new object or function on every render and passes it as a prop, the wrapped component will see a new reference and re-render even if the underlying data did not change.

Render prop issues (the classic "always re-renders" problem):

// BAD — inline arrow function creates a new function reference every render
// → Mouse re-renders, calls render(), child always re-renders
<Mouse render={({ x, y }) => <Crosshair x={x} y={y} />} />

// GOOD — stable function reference with useCallback (or define outside render)
function App() {
  const renderCrosshair = React.useCallback(
    ({ x, y }) => <Crosshair x={x} y={y} />,
    [] // stable — no deps
  );
  return <Mouse render={renderCrosshair} />;
}

For HOCs, apply React.memo to each layer independently, or memoize injected values inside the HOC with useMemo/useCallback.

Rule of thumb: Stabilise function references passed as render props with useCallback; inside HOCs, memoize injected values to avoid cascading re-renders.

The key is using generics to capture the wrapped component's prop type and subtract the injected props so callers do not have to re-supply them.

// Props injected by the HOC
interface WithAuthProps {
  currentUser: User;
}

// P = full props of wrapped component
// We subtract WithAuthProps so callers don't pass currentUser manually
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 />;

    // Cast needed because TS can't fully verify the Omit + spread
    return <WrappedComponent {...(props as P)} currentUser={user} />;
  }

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

  hoistNonReactStatics(WithAuth, WrappedComponent);
  return WithAuth;
}

// Usage — TypeScript knows currentUser is supplied by HOC, not by caller
interface DashboardProps extends WithAuthProps { title: string; }
function Dashboard({ currentUser, title }: DashboardProps) { /* ... */ }

const ProtectedDashboard = withAuth(Dashboard);
<ProtectedDashboard title="Home" /> // ✓ — no currentUser required

Rule of thumb: Use P extends InjectedProps on the generic and Omit<P, keyof InjectedProps> on the output type so the HOC's additions are invisible to callers.

Strategy 1 — Test the wrapped component in isolation. Export the unwrapped component as a named export alongside the default HOC-wrapped export. In tests, import the unwrapped version and pass the injected props manually. This keeps unit tests free of HOC side-effects.

Strategy 2 — Mock the HOC's dependencies and test the full composed component. Useful for integration tests that need to verify the HOC's interception logic (e.g., that withAuth redirects unauthenticated users).

// userList.tsx — dual export pattern
export function UserList({ currentUser }: WithAuthProps & { currentUser: User }) {
  return <div>{currentUser.name}</div>;
}
export default withAuth(UserList); // default = enhanced

// userList.test.tsx
import { UserList } from './userList'; // named — no HOC
import { render, screen } from '@testing-library/react';

test('renders user name', () => {
  render(<UserList currentUser={{ name: 'Alice', role: 'user' }} />);
  expect(screen.getByText('Alice')).toBeInTheDocument();
});

// Integration test — mock auth context
import WrappedUserList from './userList'; // default — HOC included
test('redirects when unauthenticated', () => {
  mockUseAuth({ user: null, loading: false });
  render(<WrappedUserList />, { wrapper: MemoryRouterWrapper });
  expect(mockNavigate).toHaveBeenCalledWith('/login', expect.anything());
});

Rule of thumb: Always export the base component as a named export so unit tests can bypass the HOC; use integration tests only when you need to test the HOC's own behaviour.

The decision hinges on who controls the rendered output and how the logic is consumed.

Choose render prop when… Choose HOC when…
The consumer needs to render different UI per use-site Every consumer renders roughly the same structure
You want explicit, visible data flow in JSX You want transparent wrapping (consumer doesn't change)
You need to compose render slots (header, body, footer) You need to decorate a whole page/route component
The consuming component is a function component You need to wrap class components with consistent behaviour
// Render prop — consumer decides the UI
<Toggle>
  {({ on, toggle }) =>
    on
      ? <Button onClick={toggle}>Turn Off</Button>
      : <Button onClick={toggle}>Turn On</Button>
  }
</Toggle>

// HOC — consumer is unaware of the wrapping
const AnalyticsButton = withPageViewLogger(Button);
// Button code is unchanged; analytics are injected invisibly
<AnalyticsButton onClick={handleClick}>Buy Now</AnalyticsButton>

Rule of thumb: Use render props when the consumer owns the output markup; use HOCs when you want to transparently add behaviour to an existing component.

Wrapper hell (also called "HOC hell" or "pyramid of doom") describes a component tree where many HOC layers surround a single component, making DevTools output nearly unreadable and stack traces hard to follow.

In React DevTools you see something like:

// Component tree with 5 HOCs — hard to navigate
<WithRouter>
  <WithTheme>
    <WithAuth>
      <WithLogger>
        <WithErrorBoundary>
          <Dashboard />
        </WithErrorBoundary>
      </WithLogger>
    </WithAuth>
  </WithTheme>
</WithRouter>

Solutions:

  1. Replace with hooks — collapse all five wrappers into hook calls inside Dashboard.
  2. Use compose — at least the source code reads linearly even if the tree is deep.
  3. Set meaningful displayName on each HOC so DevTools shows WithAuth(Dashboard).
// After refactoring to hooks — flat tree, same logic
function Dashboard() {
  const { params } = useRouter();
  const theme = useTheme();
  const { user } = useAuth();
  usePageLogger();
  useErrorBoundary();
  // ...
}

Rule of thumb: If DevTools shows more than two or three wrapper components around a single leaf, consider collapsing the HOC stack into custom hooks.

No — hooks cannot run inside class components. An HOC (or render prop) remains the only way to inject hook-based logic into a class component. This matters for legacy codebases or third-party libraries still using class components.

The recommended migration path is incremental:

// Step 1 — write the logic as a hook
function useTheme() {
  return React.useContext(ThemeContext);
}

// Step 2 — create a thin HOC that calls the hook and injects the result
//           This bridges class components without rewriting them
function withTheme(WrappedComponent) {
  function WithTheme(props) {
    const theme = useTheme(); // hook runs here (function component)
    return <WrappedComponent {...props} theme={theme} />;
  }
  WithTheme.displayName = `WithTheme(${WrappedComponent.name})`;
  return WithTheme;
}

// Step 3 — class component uses the HOC today
class Toolbar extends React.Component {
  render() {
    return <div style={{ background: this.props.theme.bg }}>…</div>;
  }
}
export default withTheme(Toolbar);

// Step 4 — when Toolbar is converted to a function component, drop the HOC
//           and call useTheme() directly

Rule of thumb: Write all new logic as hooks; wrap in a thin HOC only to bridge class components; remove the HOC shim when the class component is converted.

React uses displayName (or the function name as a fallback) when rendering component trees in DevTools and in error stack traces. Without it, every HOC-wrapped component shows as Anonymous or just the HOC function name, making debugging very hard when multiple HOCs are stacked.

The standard format is WithXxx(OriginalName):

function withSubscription(WrappedComponent) {
  function WithSubscription(props) {
    // ...
    return <WrappedComponent {...props} />;
  }

  // Correct format: WithHocName(WrappedName)
  WithSubscription.displayName =
    `WithSubscription(${
      WrappedComponent.displayName  // already set display name
      ?? WrappedComponent.name      // function/class name
      ?? 'Component'               // last resort fallback
    })`;

  return WithSubscription;
}

// DevTools now shows: WithSubscription(Dashboard)
// instead of:         WithSubscription or Anonymous

Some teams use a getDisplayName helper to deduplicate the ternary chain across all HOCs in a codebase.

Rule of thumb: Always set displayName using the WithXxx(InnerName) format; check all three sources (displayName, name, fallback) in that priority order.

Yes. An error boundary must be a class component (React's componentDidCatch API is not yet available as a hook), but you can expose its caught error via a render prop so the caller controls the fallback UI — combining the two patterns.

class ErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch(error, info) {
    reportError(error, info); // side-effect: logging service
  }

  render() {
    const { error } = this.state;
    const { children, fallback } = this.props;

    if (error) {
      // Delegate fallback UI to the caller via render prop
      return typeof fallback === 'function'
        ? fallback(error)       // render prop form
        : fallback ?? <p>Something went wrong.</p>;
    }

    return children;
  }
}

// Caller controls the fallback UI
<ErrorBoundary fallback={err => <ErrorPage message={err.message} />}>
  <UserProfile userId={id} />
</ErrorBoundary>

This pattern is exactly how the popular react-error-boundary library works internally (renderError / FallbackComponent prop).

Rule of thumb: Error boundary logic stays in a class component; expose the fallback slot as a render prop so each call site can render appropriate recovery UI.

React.PureComponent and React.memo perform a shallow comparison of props. An inline arrow function creates a new function object on every parent render, so the render-prop component always sees a changed render prop — the shallow compare fails, PureComponent's optimisation is bypassed, and the component re-renders unconditionally.

class Mouse extends React.PureComponent {
  // shallow compare on props.render — always a new reference → always re-renders
  render() { return this.props.render(this.state); }
}

// BAD — new arrow function on every render of App
class App extends React.Component {
  render() {
    return <Mouse render={pos => <Cat {...pos} />} />;  // new fn each time!
  }
}

// GOOD — define the render function as an instance method; reference is stable
class App extends React.Component {
  renderCat = pos => <Cat {...pos} />;  // stable class field

  render() {
    return <Mouse render={this.renderCat} />;
  }
}

// GOOD (function component equivalent) — useCallback for stable reference
function App() {
  const renderCat = React.useCallback(pos => <Cat {...pos} />, []);
  return <Mouse render={renderCat} />;
}

Rule of thumb: Never pass an inline arrow function as a render prop to a PureComponent or memo-wrapped component; stabilise the reference with useCallback or a class instance method.

More ways to practice

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

or
Join our WhatsApp Channel