React

The Render Prop Pattern in React

How the render prop pattern lets you share behaviour between components, why hooks mostly replaced it, and the cases where render props still earn their...

When I first learned React, the render prop pattern felt like magic. A component that didn't render its own UI, that just gave me data and let me render whatever I wanted. Then hooks arrived and render props quietly fell out of fashion. They never went away, though, and understanding them still helps you read older code, work with certain libraries, and recognise the handful of cases where a render prop is still the cleanest fit.

This is what I wish I'd read when I kept seeing <Component>{(state) => ...}</Component> in a codebase and wasn't sure what I was looking at.

What a render prop actually is

A render prop is a prop that is a function. That function returns JSX. Instead of a component rendering its own children or some fixed UI, it calls the function you passed in and uses the result.

Here is the shape in plain terms:

function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });

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

  return render(pos);
}

MouseTracker owns the tracking logic. It does not own the UI. You decide what to render using the position it gives you.

<MouseTracker render={(pos) => <p>Mouse is at {pos.x}, {pos.y}</p>} />

The pattern scales to anything stateful you want to share: fetched data, form state, drag state, scroll position. The shared thing is always logic, and the render prop is how that logic hands control back to the caller.

The "children as a function" variant

Instead of a prop literally named render, you often see the function passed as children.

<MouseTracker>
  {(pos) => <p>{pos.x}, {pos.y}</p>}
</MouseTracker>

This reads better in JSX because it looks like ordinary composition. The mechanics are identical. Downshift, React Motion, and older Formik APIs all leaned on this style.

Why hooks mostly replaced it

Hooks let you extract the same logic without wrapping the consumer in another component:

function useMouse() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handler);
    return () => window.removeEventListener("mousemove", handler);
  }, []);
  return pos;
}

function Display() {
  const pos = useMouse();
  return <p>{pos.x}, {pos.y}</p>;
}

Three things get better here:

  1. No extra component in the tree. Render props created a wrapping element that could interfere with layout, flex children, and list keys.
  2. Composition is flat. With render props, sharing two behaviours meant nesting two components. Hooks read top-to-bottom in the function body.
  3. TypeScript is easier. Typing render as a function that accepts state and returns JSX is fine once, awkward when generic.

If you are writing new code, reach for a hook first. See custom hooks for the patterns that replace most render prop use cases.

Where render props still earn their keep

A few situations still favour render props.

Library boundaries where the API lives in JSX. If you are writing a headless library component that wants to expose state through JSX composition rather than a hook, a render prop is a clean API. Downshift's top-level component exposes a render prop because the JSX-shaped API is pleasant for form combo boxes where the caller wires up inputs, menus, and items.

Animation and layout libraries that hand you measured values. React Virtuoso, some virtualised list libraries, and certain motion APIs use the render prop style to give you a callback with measured values or motion state. A hook cannot easily hand you values that only exist once the child has been measured in the DOM.

Boundaries that cannot be hooks. Error boundaries still have to be class components. If you want to offer a "reset" function alongside the boundary, a render prop on the class component is a clean API:

<ErrorBoundary fallback={(error, reset) => <FailedPanel onRetry={reset} error={error} />}>
  <Dashboard />
</ErrorBoundary>

Error boundaries in practice covers the boundary mechanics. The render prop on the fallback is how you pass reset back out.

Reading render props in older code

If you inherit a codebase from the 2018-2020 era you will see render props everywhere. A few tips for reading them quickly:

  • Look at what the function argument is. That is the shared state or helpers.
  • Look at what the function returns. That is the UI for this particular use.
  • If the component's children is a function, treat it as a render prop, not real children.

A common stumble is seeing <Formik>{({ values, handleSubmit }) => (...)}</Formik> and reading it as children. It is not. It is a function being called by Formik. If you mentally translate it to const { values, handleSubmit } = useFormik(...) the code stops feeling strange.

When I would still reach for one today

Most of the time I write a hook. The case where I still write a render prop is when a component has to own a DOM subtree and also expose state to whatever renders inside it. A <Resizable> panel that gives you the current width, a <Draggable> container that gives you the drag position, a <Dropzone> that gives you isOver and isOverAccepted. The component has to be in the tree to attach refs and event handlers, and the render prop gives the caller the state those handlers produce. You can build the same thing with a context plus a hook, but the render prop keeps the API to a single component with one prop and no separate hook import.

The mental model I land on

A render prop inverts the usual render-then-compose order. Normal components say "here is what I will render". Render prop components say "here is what I know, you render". Once you see it that way, the pattern stops looking quirky. It is a way of handing control of the output to the caller while keeping ownership of state and side effects.

Hooks did not kill render props. They replaced them as the default. The pattern still shows up in libraries that need a JSX-shaped API, in components that own a DOM subtree, and in any boundary that cannot be expressed as a hook. Knowing when a render prop beats a hook means you can read old React code fluently and pick the right tool when the hook version feels forced.

← Older
The Virtual DOM and How React Reconciliation Works
Newer →
The React Suspense Mental Model

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.