Render Props vs. Children-as-Function Pattern: Still Relevant Today?
AI generated
{ }
React 19 · Patterns · API Design
Render Props vs. Children-as-Function Pattern
Still relevant today?

Before hooks existed, render props and children-as-function were the answer to sharing logic between components. An honest look at where they still offer real value in the age of hooks and where custom hooks are clearly superior.

14 min read Component Patterns API Design Legacy vs. Modern

1. What render props were originally meant to solve

Before hooks were introduced in React 16.8, there was barely any way to share stateful logic between components without resorting to inheritance or higher-order components. The render props pattern solved this by having a component accept a function as a prop instead of static children, a function the component itself called with internal state or computed values. The calling side then decided freely how to render those values.

A typical MouseTracker component, for example, tracked the mouse position internally and called a render prop with the current coordinates. Any component wanting to use this logic did not have to implement it itself, it could simply act as a consumer following the render prop contract. At the time, that was a real improvement over inheritance chains and confusing HOC nesting.


class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({ x: event.clientX, y: event.clientY });
  };

  render() {
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

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

2. Children-as-function as a variant

A closely related variant uses children itself as a function instead of a separate render prop. Instead of <MouseTracker render={fn} />, you write <MouseTracker>{fn}</MouseTracker>, since React simply treats children as an arbitrary value, including a function. The practical difference is small, but the readability in JSX feels more natural to many, because the structure resembles ordinary component nesting more closely.

Both variants share the same core idea: a component encapsulates logic and state but leaves the rendering entirely to the calling side. The pattern is therefore often described as 'inversion of control', because control over what gets rendered is handed from the logic component to the caller.


function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  return (
    <div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
      {children(position)}
    </div>
  );
}

// Usage
<MouseTracker>
  {({ x, y }) => <p>Mouse at ({x}, {y})</p>}
</MouseTracker>

3. What hooks changed about the starting point

With custom hooks, the same logic reuse can be achieved without introducing an extra wrapper component into the tree. A useMousePosition hook encapsulates exactly the same state logic as the MouseTracker component, but returns the values directly as a return value, without a component having to outsource its own render logic into a foreign function.

The key difference is structural: render props inevitably create an extra layer of components in the tree, with its own lifecycle and its own entry in the React DevTools. A custom hook, by contrast, blends seamlessly into the calling component, no extra nesting occurs, and the code reads linearly instead of functionally nested.


function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

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

  return position;
}

// Usage -- no wrapper component needed
function Cursor() {
  const { x, y } = useMousePosition();
  return <p>Mouse at ({x}, {y})</p>;
}

4. The wrapper hell problem with nested render props

As soon as multiple render props components need to be combined, for example mouse position, window size, and authentication status at the same time, deeply nested JSX quickly emerges, colloquially known as 'wrapper hell' or 'callback hell'. Every additional logic source adds another level of nesting, and the actual rendering code keeps drifting further to the right.

With custom hooks, this problem disappears entirely, because multiple hooks are simply called one after another in the same component, without producing any extra nesting. This is one of the strongest practical reasons custom hooks have become the standard for logic reuse in application code.


// Render props variant: wrapper hell
<MouseTracker>
  {(mouse) => (
    <WindowSize>
      {(size) => (
        <Auth>
          {(user) => (
            <Dashboard mouse={mouse} size={size} user={user} />
          )}
        </Auth>
      )}
    </WindowSize>
  )}
</MouseTracker>

// Hooks variant: flat, linear
function Dashboard() {
  const mouse = useMousePosition();
  const size = useWindowSize();
  const user = useAuth();
  return <DashboardView mouse={mouse} size={size} user={user} />;
}

5. Where render props still make real sense

One area where the pattern has not become obsolete is library APIs, where the rendering of a UI element should deliberately be left to the consuming application, while the library itself only controls behavior and state. Libraries for drag-and-drop data lists, virtualized lists, or form fields frequently use render props for exactly this reason, because a custom hook alone is not enough to also control the DOM structure or event handler binding at a specific point in the tree.

The reason is that a hook can only return values but has no control over the JSX structure itself. A render prop component, on the other hand, can additionally control exactly where in the tree an interaction is registered, for example onMouseDown on a specific wrapper element, which is not possible with plain hooks unless the caller wires it up manually.

6. Practical example: headless components with render props

A concrete example is a sortable table component, where the library manages sorting logic and state but leaves header cell and row rendering entirely to the application. The render prop contract ensures that click handlers and sort status are correctly bound to the right DOM elements, while the visual presentation stays free.

Such 'headless components' are a legitimate and widely used case for render props in practice, because the separation between behavior and presentation matches exactly the core idea of the pattern. Libraries like Downshift for autocomplete behavior historically used exactly this pattern, though many of them now also offer hook-based APIs in parallel.


function SortableTable({ data, renderHeader, renderRow }) {
  const [sortKey, setSortKey] = useState(null);
  const sorted = useMemo(() => sortData(data, sortKey), [data, sortKey]);

  return (
    <table>
      <thead>{renderHeader({ sortKey, onSort: setSortKey })}</thead>
      <tbody>{sorted.map((row) => renderRow(row))}</tbody>
    </table>
  );
}

7. Hybrid APIs: offering a custom hook and a render prop side by side

Many established libraries do not settle the trade-off permanently in favor of one pattern, they offer both access paths in parallel. Internally, a custom hook encapsulates the actual state logic, and a thin render props or children-as-function component is built merely as a wrapper around that very hook. This way, users who only need plain value access benefit from the lean hook, while users who additionally need DOM control can use the wrapper component, without the library having to maintain the logic twice.

This pattern is especially useful during migration phases, when a library was historically built with render props and existing consumers should not be forced to switch immediately. The hook becomes the actual source of truth, while the render props component stays around as a thin, backward-compatible shell, saving maintenance effort and serving both audiences.


// The hook is the actual source of logic
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue((v) => !v), []);
  return [value, toggle];
}

// Render props component as a thin wrapper around the hook
function Toggle({ children, initial }) {
  const [value, toggle] = useToggle(initial);
  return children({ value, toggle });
}

8. When custom hooks are clearly the better choice

For the vast majority of logic reuse in application code, such as data fetching, form state, media queries, or debouncing, a custom hook is almost always the better choice. It is easier to test, since it can be checked in isolation via renderHook without a rendering context, it creates no extra component tree, and it combines effortlessly with other hooks.

From a TypeScript perspective, hooks are also more pleasant: a hook's return type can be typed precisely, while render props additionally require typing the signature of the callback function, which in practice leads to more type definitions for the same benefit. As soon as the question becomes whether the consuming side also needs control over the concrete DOM structure at several points, the balance tips back toward render props or the headless component pattern.

9. A decision guide for your own codebase

The pragmatic rule of thumb is: if only values or state need to be shared, a custom hook is the right choice. If additional control over specific DOM bindings, event handler placement, or a public, framework-independent library API is needed, the render props or children-as-function pattern remains a valid, deliberate design decision.

It is important not to keep the pattern in your own application code out of habit, just because it was established in an older codebase. Refactoring to custom hooks pays off in most cases as soon as the render props component only serves internal application code and does not represent a public, consuming API.

Criterion Render Props / Children Function Custom Hooks
Extra component layer in the tree yes, always no
Combining multiple logic sources nested (wrapper hell) flat, linear
Control over DOM structure/event binding yes, controllable from the caller side only limited
Isolated testing without rendering harder easy via renderHook
Typical use today headless component libraries internal application logic reuse

Mironsoft

React architecture, performance, and Magento frontend integration

React frontends that stay fast instead of slowing down with every feature?

We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.

Performance Audit

Systematically measuring and fixing re-renders, bundle size, and load times.

State Architecture

Cleanly separating context, client state, and server state instead of mixing everything.

Magento Integration

Building robust, type-safe GraphQL or REST integration with Magento.

10. Summary

Render Props: The Key Facts at a Glance

Historical purpose

Render props solved logic reuse before hooks were introduced.

Biggest weakness

Nesting multiple render props components leads to wrapper hell.

Modern alternative

Custom hooks for pure state and logic reuse without DOM control.

Remaining use case

Headless component libraries that need DOM structure control.

11. FAQ: Render Props: The Key Facts at a Glance

1Is the render props pattern outdated in React 19?
Not generally outdated, but replaced by custom hooks for most use cases. It remains relevant for library APIs where the consuming side needs control over the DOM structure.
2What is the difference between render props and children-as-function?
Functionally identical, the difference is purely syntactic. Render props pass a separate prop such as render, while children-as-function uses children itself as a function.
3Why does nested render props lead to wrapper hell?
Because every additional logic source requires another component layer with its own callback function. Multiple combined render props components nest deeply into each other, which makes JSX hard to read.
4Can a custom hook replace render props in every case?
No. A hook can only return values but cannot take control of the concrete DOM structure or event handler placement at multiple points in the tree, which is often needed for headless component libraries.
5What are headless components and what do they have to do with render props?
Headless components fully separate behavior and state from the visual presentation. Render props are one of the classic tools used to implement this separation technically, since they leave full control over rendering to the caller.
6Should I refactor existing render props code in my application?
In most cases yes, provided the component only serves internal application code. Once it represents a public library API that hands DOM control to the consuming side, refactoring is not strictly necessary.
7Why are custom hooks easier to test than render props?
Custom hooks can be tested in isolation with renderHook, without needing a full component to render. Render props require rendering the wrapper component including the callback function, which makes the test setup more complex.
8Does a render props component really cause measurable overhead?
The pure rendering overhead is usually small, the real issue is structural complexity from extra component layers in the tree, which hurts debugging and readability, not primarily performance.
9Are there libraries that offer both render props and hooks in parallel?
Yes, many established UI and behavior libraries now offer both APIs in parallel, to support both existing render props users and new, hook-based codebases.
10How do I cleanly type a render prop component in TypeScript?
You define a generic type for the callback function passed as a prop, including parameter type and return type. This is possible but requires more type definition effort than a comparable custom hook with a typed return value.