React Render Props Pattern

The render props pattern shares behavior between components by passing a function as a prop. The receiving component calls this function to determine what to render. It is a classic code-sharing technique from before React hooks — and understanding it helps you read older React libraries and codebases.

The Core Idea

Instead of a component deciding on its own what to render, the parent passes a function that decides. The component provides behavior and data; the function prop decides appearance.

Diagram: Render props flow

Parent Component
    │
    │ render={(data) => <Display value={data} />}
    ▼
MouseTracker (provides mouse position behavior)
    │
    │ calls render({ x: 120, y: 80 })
    ▼
<Display value={{ x: 120, y: 80 }} /> renders on screen

Basic Example: Mouse Position Tracker

import { useState } from "react";

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

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

  return (
    <div
      style={{ height: "300px", border: "1px solid #ccc" }}
      onMouseMove={handleMouseMove}
    >
      {/* Call the render function, passing position data */}
      {render(position)}
    </div>
  );
}

// Two different uses of the SAME behavior with different output
function App() {
  return (
    <div>
      {/* Use 1: Show coordinates */}
      <MouseTracker
        render={({ x, y }) => <p>Mouse at: {x}, {y}</p>}
      />

      {/* Use 2: Show a dot following the mouse */}
      <MouseTracker
        render={({ x, y }) => (
          <div
            style={{
              position: "absolute",
              left: x - 8,
              top: y - 8,
              width: 16,
              height: 16,
              borderRadius: "50%",
              background: "blue",
            }}
          />
        )}
      />
    </div>
  );
}

Both uses share the mouse-tracking behavior from MouseTracker but render completely different output.

The children as a Function Pattern

Instead of a prop named render, many libraries use the children prop as a function. This is sometimes called "children as a function" or "function as children."

function DataProvider({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((result) => {
        setData(result);
        setLoading(false);
      });
  }, [url]);

  // children is called as a function with the current state
  return children({ data, loading });
}

// Usage: children is a function
function App() {
  return (
    <DataProvider url="https://jsonplaceholder.typicode.com/users">
      {({ data: users, loading }) => {
        if (loading) return <p>Loading...</p>;
        return (
          <ul>
            {users.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        );
      }}
    </DataProvider>
  );
}

Reusable Toggle with Render Props

function Toggle({ render }) {
  const [on, setOn] = useState(false);
  const toggle = () => setOn((prev) => !prev);

  return render({ on, toggle });
}

// Same Toggle logic used for different UIs
function App() {
  return (
    <div>
      {/* Use 1: Simple button */}
      <Toggle
        render={({ on, toggle }) => (
          <button onClick={toggle}>{on ? "ON" : "OFF"}</button>
        )}
      />

      {/* Use 2: Toggle switch appearance */}
      <Toggle
        render={({ on, toggle }) => (
          <div
            onClick={toggle}
            style={{
              width: 60, height: 30,
              background: on ? "green" : "gray",
              borderRadius: 15,
              cursor: "pointer",
            }}
          />
        )}
      />
    </div>
  );
}

Render Props vs Custom Hooks

Custom hooks largely replaced render props because hooks share behavior without adding component layers to the tree.

/* RENDER PROP (classic) */
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => <Cursor x={x} y={y} />}
    />
  );
}

/* CUSTOM HOOK (modern equivalent) */
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handler);
    return () => window.removeEventListener("mousemove", handler);
  }, []);
  return position;
}

function App() {
  const { x, y } = useMousePosition();
  return <Cursor x={x} y={y} />;
}

The custom hook version is shorter, adds no wrapper component to the tree, and composes more cleanly with other hooks.

When Render Props Still Make Sense

  • When working with libraries that expose render prop APIs (Formik, Downshift, React Router's older versions)
  • When the component genuinely needs to control the render timing or output format
  • When the shared logic requires JSX structure the hook cannot provide

Performance Consideration

Inline render prop functions create a new function reference every render — this prevents child components wrapped in React.memo from skipping re-renders.

// BAD — new function every render
<Toggle render={({ on, toggle }) => <Button on={on} toggle={toggle} />} />

// BETTER — define the function outside
const renderToggleButton = ({ on, toggle }) => <Button on={on} toggle={toggle} />;

function App() {
  return <Toggle render={renderToggleButton} />;
}

Summary

The render props pattern shares behavior across components by passing a function as a prop. The component with behavior calls the function prop to produce output, separating the logic from the presentation. The children prop works as a function in the same way — some libraries call this "function as children." Render props appear widely in older React libraries and codebases predating hooks. Custom hooks largely replaced render props for new code because they achieve the same reuse without adding wrapper components to the tree. Knowing render props helps you read and work with legacy code and established libraries that still use the pattern.

Leave a Comment

Your email address will not be published. Required fields are marked *