Skip to main content
Technology & EngineeringReact Patterns181 lines

Render Props

Render props pattern for sharing cross-cutting logic through function-as-children and render callbacks

Quick Summary18 lines
You are an expert in the Render Props pattern for building React applications.

## Key Points

- **Function as Children**: The most common variant — `children` is a function that receives data and returns JSX.
- **Render Callback Prop**: A named prop (e.g., `render`, `renderItem`) that serves the same purpose.
- **Separation of Concerns**: Logic lives inside the provider component; presentation lives in the consumer's render function.
- **Composability**: Multiple render-prop components can be nested to combine behaviors.
- Type the render function's parameters explicitly so consumers get full IntelliSense.
- Use `children` as the render prop when the component has a single render callback; use named props (`renderItem`, `renderHeader`) when there are multiple.
- Memoize data passed to the render function if it is an object or array created on each render.
- Consider extracting the logic into a custom hook if the component does not need to control the DOM wrapper.
- Document the shape of the arguments the render function receives.
- **Inline function re-creation**: Defining the render function inline on every render can cause unnecessary re-renders in children. Extract it or use `useCallback` when performance matters.
- **Callback hell**: Deeply nesting render props becomes unreadable. Flatten by composing with custom hooks or by breaking the tree into smaller components.
- **Ignoring `key` props**: When render props produce lists, the consumer must provide stable keys.
skilldb get react-patterns-skills/Render PropsFull skill: 181 lines
Paste into your CLAUDE.md or agent config

Render Props — React Patterns

You are an expert in the Render Props pattern for building React applications.

Overview

Render props is a technique where a component receives a function as a prop (or as children) and calls that function to determine what to render. The function receives internal state or behavior as arguments, letting consumers control rendering while the component owns the logic. Although custom hooks have replaced many render-prop use cases, the pattern remains valuable for components that need to share logic while controlling the rendering lifecycle.

Core Philosophy

Render props solve the tension between reusable logic and custom presentation. A component that tracks mouse position, manages a dropdown's open/close state, or handles virtualized scrolling contains valuable logic, but the moment it dictates how that logic is rendered, it becomes rigid. Render props let the logic-owning component hand control of the visual output to the consumer, creating a clean separation between "what it does" and "what it looks like."

This pattern predates hooks and was the primary way to share cross-cutting behavior in class component codebases. While custom hooks have absorbed many of those use cases, render props remain the right choice when the logic provider needs to control the DOM wrapper (e.g., attaching event listeners to a specific element) or when you want to expose a component-level API that participates in React's rendering lifecycle rather than being called imperatively. Render props are also the natural API for components like virtualized lists and drag-and-drop containers that need to own a DOM node while letting consumers define what goes inside.

The guiding principle is explicitness. The render function's signature documents exactly what data and callbacks the consumer receives. Unlike higher-order components, which inject props invisibly and create naming collisions, render props make the contract visible at the call site. Every piece of shared state is passed as a named argument, and the consumer decides what to do with it.

Core Concepts

  • Function as Children: The most common variant — children is a function that receives data and returns JSX.
  • Render Callback Prop: A named prop (e.g., render, renderItem) that serves the same purpose.
  • Separation of Concerns: Logic lives inside the provider component; presentation lives in the consumer's render function.
  • Composability: Multiple render-prop components can be nested to combine behaviors.

Implementation Patterns

Mouse Tracker

import { useState, ReactNode } from "react";

interface Position {
  x: number;
  y: number;
}

interface MouseTrackerProps {
  children: (position: Position) => ReactNode;
}

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

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

// Usage
function App() {
  return (
    <MouseTracker>
      {({ x, y }) => (
        <p>
          Mouse is at ({x}, {y})
        </p>
      )}
    </MouseTracker>
  );
}

Fetch Component with Render Prop

import { useState, useEffect, ReactNode } from "react";

interface FetchProps<T> {
  url: string;
  children: (state: { data: T | null; loading: boolean; error: Error | null }) => ReactNode;
}

function Fetch<T>({ url, children }: FetchProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((json) => {
        if (!cancelled) {
          setData(json);
          setLoading(false);
        }
      })
      .catch((err) => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });
    return () => {
      cancelled = true;
    };
  }, [url]);

  return <>{children({ data, loading, error })}</>;
}

// Usage
<Fetch<User[]> url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} />;
    return <UserList users={data!} />;
  }}
</Fetch>

Named Render Props for Lists

interface VirtualListProps<T> {
  items: T[];
  itemHeight: number;
  renderItem: (item: T, index: number) => ReactNode;
  renderEmpty?: () => ReactNode;
}

function VirtualList<T>({ items, itemHeight, renderItem, renderEmpty }: VirtualListProps<T>) {
  if (items.length === 0 && renderEmpty) {
    return <>{renderEmpty()}</>;
  }

  return (
    <div style={{ height: items.length * itemHeight, position: "relative" }}>
      {items.map((item, index) => (
        <div
          key={index}
          style={{ position: "absolute", top: index * itemHeight, height: itemHeight }}
        >
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  );
}

Best Practices

  • Type the render function's parameters explicitly so consumers get full IntelliSense.
  • Use children as the render prop when the component has a single render callback; use named props (renderItem, renderHeader) when there are multiple.
  • Memoize data passed to the render function if it is an object or array created on each render.
  • Consider extracting the logic into a custom hook if the component does not need to control the DOM wrapper.
  • Document the shape of the arguments the render function receives.

Common Pitfalls

  • Inline function re-creation: Defining the render function inline on every render can cause unnecessary re-renders in children. Extract it or use useCallback when performance matters.
  • Callback hell: Deeply nesting render props becomes unreadable. Flatten by composing with custom hooks or by breaking the tree into smaller components.
  • Ignoring key props: When render props produce lists, the consumer must provide stable keys.
  • Mixing patterns: Avoid accepting both children as a render function and children as regular JSX — pick one API and be explicit.

Anti-Patterns

  • Render prop where a hook would suffice: Using a render-prop component solely to share stateful logic when the component does not need to own a DOM node. If the provider renders nothing but <>{children(data)}</>, a custom hook would be simpler, avoid an extra component in the tree, and eliminate the nesting.

  • Deeply nested render-prop "pyramids": Composing three or more render-prop components inline, creating callback hell that is impossible to read or debug. Flatten by extracting inner render functions into named components or by combining the logic into a single custom hook.

  • Unstable render function references: Passing an arrow function as the render prop on every render without memoization. When the render-prop component uses React.memo or shouldComponentUpdate, the new function reference defeats the optimization and causes unnecessary re-renders of the entire subtree.

  • Overloading the render function's arguments: Passing ten or more arguments to the render callback, turning the call site into a destructuring exercise. Keep the argument surface small and cohesive; if consumers need different slices of data, provide multiple named render props (e.g., renderHeader, renderItem, renderEmpty).

  • Dual-mode children: Accepting children as both a render function and regular ReactNode, then branching internally with typeof children === "function". This creates an ambiguous API where TypeScript cannot enforce the correct usage, and consumers cannot tell from the call site which mode is active.

Install this skill directly: skilldb add react-patterns-skills

Get CLI access →