Skip to solution
mediumFrontend

What are render props and how do they work?

1.0k views
01

Understand 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.

patternsreusability
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes props and state. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The memoisation measurement in section 5 was produced by counting real renders on

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same behaviour as a render prop, as children-as-a-function, and as a Hook
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 36 of 119 decoded in the React.js track. One more won't hurt.

Back to track