Skip to the solution
MediumFrontend6 min read

What are render props and how do they work?

1.0k views

The problem

Question presented to candidate: "What is the render props pattern, what problem did it solve, and do you still use it?"

What a strong answer should cover:

  • A render prop is a prop whose value is a function returning UI. The component owns some state or behaviour and calls that function with it, letting the caller decide the markup.
  • children as a function is the same pattern with a different prop name.
  • The problem it solved: sharing stateful logic before Hooks existed, without the wrapper-component drawbacks of HOCs.
  • Key advantage over HOCs: composition happens at render time, so it can use values from the surrounding render, and there are no prop-name collisions or lost statics.
  • The costs: nesting ("callback hell" in JSX) when several are combined, and an inline function is a new identity every render, which defeats React.memo on the receiving component.
  • Custom Hooks replaced it for pure logic sharing β€” no nesting, no identity problem.
  • Where it survives legitimately: when the component must control rendering, not just supply data β€” virtualised lists, headless UI components, data tables, and libraries that need to own the loop.

Clarifying questions expected:

  • "Is the shared thing logic, or does the component need to control the rendering?" β€” that decides Hook versus render prop.

Code / implementation expected: Yes β€” a render prop component, ideally with the Hook equivalent beside it.

  • patterns
  • reusability

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 8 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactThe same behaviour as a render prop, as children-as-a-function, and as a Hook
import { useState, useEffect, useCallback, memo } from "react";

// ── The shared behaviour, as a render prop component ───────────────────────
// It owns the pointer state and has no opinion about how it is displayed.
function PointerTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const onMove = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("pointermove", onMove);
    return () => window.removeEventListener("pointermove", onMove);
  }, []);
  return render(pos);
}

// ── The same thing with children as the function ──────────────────────────
function PointerTracker2({ children }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const onMove = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("pointermove", onMove);
    return () => window.removeEventListener("pointermove", onMove);
  }, []);
  return children(pos);
}

// ── The modern equivalent: a custom Hook. Flat, no nesting. ────────────────
function usePointer() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const onMove = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("pointermove", onMove);
    return () => window.removeEventListener("pointermove", onMove);
  }, []);
  return pos;
}

function WithHook() {
  const { x, y } = usePointer();
  return <Readout label="custom Hook" x={x} y={y} />;
}

function Readout({ label, x, y }) {
  return (
    <p style={{ margin: "6px 0" }}>
      <code style={{ minWidth: 180, display: "inline-block" }}>{label}</code>
      x: <strong>{x}</strong> y: <strong>{y}</strong>
    </p>
  );
}

// A memoised consumer, to show the identity cost of an inline render prop.
let memoRenders = 0;
const MemoBox = memo(function MemoBox({ render }) {
  memoRenders++;
  return <p style={{ margin: "6px 0" }}>memoised child renders: <strong>{memoRenders}</strong>{render()}</p>;
});

export default function App() {
  const [tick, setTick] = useState(0);
  // Stable identity, so MemoBox can actually bail out. Remove the useCallback
  // and the counter climbs on every click instead of staying at 1.
  const stableRender = useCallback(() => null, []);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <h4 style={{ marginTop: 0 }}>Move your pointer over this panel</h4>

      {/* render prop */}
      <PointerTracker render={({ x, y }) => <Readout label="render prop" x={x} y={y} />} />

      {/* children as a function */}
      <PointerTracker2>
        {({ x, y }) => <Readout label="children as a function" x={x} y={y} />}
      </PointerTracker2>

      {/* custom Hook */}
      <WithHook />

      <hr />
      <MemoBox render={stableRender} />
      <button onClick={() => setTick((t) => t + 1)}>Re-render the parent ({tick})</button>
      <p style={{ color: "#666", fontSize: 13 }}>
        All three read the same pointer position. The Hook version is flattest β€”
        no nesting, no function prop, and nothing to stabilise.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 36 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

Explain the concept of reconciliation in React.

Continue