Render Props

[!NOTE] The term Render Prop refers to a technique for sharing code between React components using a prop whose value is a function.

The Problem: Sharing Stateful Logic

Imagine you are building a dashboard. You need a component that tracks the mouse position to draw a custom crosshair. You build a <MouseTracker> component that listens to mousemove events and renders the crosshair.

Later, your product manager asks for a feature where moving the mouse reveals a “hidden secret” using a spotlight effect.

The logic (tracking x and y coordinates) is exactly the same, but the UI (crosshair vs. spotlight) is completely different.

How do you share the stateful logic without duplicating the code?

If you use inheritance, React components don’t easily share lifecycle methods without brittle class hierarchies. If you try to pass the UI into the tracker as a generic child, how does the child get the x and y data?

The Concept (The Solution)

Instead of a component rendering its own hardcoded UI, it calls a function (passed as a prop) to determine what to render. This allows you to dynamically inject logic and state into whatever UI component you want.

Think of it as a “hole” in the component. The component says: “I will do all the hard work of tracking state, but when it comes time to draw something on the screen, I will call the function you gave me and pass the data to it.”

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

Interactive Demo: Mouse Tracker

This is the classic example of Render Props. The Mouse component encapsulates the logic of tracking the cursor position. It exposes the x and y coordinates via a render prop, allowing the consumer to render anything based on that data (e.g., a crosshair, a cat, or just coordinates).

Render as:

Implementation

Let’s look at how to implement the Mouse component using a render prop.

1. The Logic Component

The Mouse component handles the state (x, y) and the event listeners. Instead of rendering UI, it calls props.render(state).

import React, { useState, MouseEvent, ReactNode } from 'react';

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

interface MouseProps {
  render: (state: MouseState) => ReactNode;
}

const Mouse = ({ render }: MouseProps) => {
  const [position, setPosition] = useState<MouseState>({ x: 0, y: 0 });

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

  return (
    <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
      {/* Delegate rendering to the prop! */}
      {render(position)}
    </div>
  );
};

2. The Consumer

Now we can use Mouse to render whatever we want using the coordinate data.

const App = () => {
  return (
    <div className="app">
      <h1>Move your mouse!</h1>

      {/* Render a Cat */}
      <Mouse render={({ x, y }) => (
        <span style={{ position: 'absolute', left: x, top: y }}>
          🐱
        </span>
      )} />

      {/* OR Render coordinates */}
      <Mouse render={({ x, y }) => (
        <p>Current position: {x}, {y}</p>
      )} />
    </div>
  );
};

Visualizing Data Flow

Parent Component <Mouse /> State: { x, y } Calls render(state) UI Output args

Children as a Function

Technically, you don’t need a prop named render. You can use the children prop!

<Mouse>
  {({ x, y }) => (
    <p>The position is {x}, {y}</p>
  )}
</Mouse>

Implementation:

const Mouse = ({ children }) => {
  // ... logic ...
  return children(position);
}

Render Props vs Hooks

Before React Hooks (v16.8), Render Props were the primary way to share logic. Now, Custom Hooks are usually preferred because they are cleaner, easier to compose, and avoid “wrapper hell” (deep nesting in JSX).

With Hooks:

const { x, y } = useMousePosition();
return <div style={{ left: x, top: y }}>🐱</div>;

The Pitfall: Inline Function Re-rendering

A critical edge case with Render Props is performance. If you pass an inline arrow function to the render prop, React creates a new function reference on every single render of the parent component.

// ⚠️ BAD: Creates a new function reference every render
<Mouse render={({ x, y }) => <p>{x}, {y}</p>} />

If the Mouse component is wrapped in React.memo or PureComponent, the memoization will be defeated because the render prop changes reference every time, causing unnecessary re-renders.

The Fix: Define the render function as an instance method (in class components) or use useCallback (in functional components) to keep the reference stable.

// ✅ GOOD: Stable function reference
const renderMouse = useCallback(({ x, y }) => {
  return <p>{x}, {y}</p>;
}, []);

return <Mouse render={renderMouse} />;

Why learn Render Props today?

  1. Libraries: Many foundational libraries (like Formik, React Router v5, Downshift) still use render props heavily for complex UI composition.
  2. Inversion of Control: Render props provide excellent “Inversion of Control”. The child handles the state, but the parent retains total control over the rendering. Hooks handle state, but don’t inherently structure the UI tree.
  3. Dynamic Rendering: When you need to render something completely different based on state, a render prop is often more flexible than a hook + if/else statements.

Summary

  • Render Props enable logic reuse by passing a function as a prop.
  • The component handles logic and delegates rendering to the parent.
  • Often implemented using the children prop.
  • Largely replaced by Hooks for logic reuse, but still relevant for UI composition.