Render props and hooks are both patterns for sharing logic in React, but they solve different problems and shine in different scenarios.

Let’s see what a component using render props looks like in action. Imagine we have a MouseTracker component that tracks the mouse’s X and Y coordinates.

// MouseTracker.js
import React, { Component } from 'react';

class MouseTracker extends Component {
  constructor(props) {
    super(props);
    this.state = { x: 0, y: 0 };
  }

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

  render() {
    // The render prop is a function that receives the component's state
    // and returns JSX.
    return (
      <div onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

// Usage in another component
function App() {
  return (
    <div>
      <h1>Move the mouse around!</h1>
      <MouseTracker
        render={({ x, y }) => (
          <h1>The mouse position is ({x}, {y})</h1>
        )}
      />
    </div>
  );
}

In this example, MouseTracker uses a render prop named render. It calls this.props.render and passes its internal state (x and y coordinates) as arguments. The App component provides the actual JSX to be rendered, which receives the mouse coordinates and displays them. The MouseTracker component doesn’t know what will be rendered, only that it needs to provide the mouse coordinates to whatever is passed as the render prop. This is the essence of render props: sharing behavior by passing a function as a prop that returns React elements.

Now, let’s contrast this with hooks. Hooks allow you to extract stateful logic from components so it can be tested independently and reused. Here’s a custom hook that does the same thing as our MouseTracker:

// useMousePosition.js
import { useState, useEffect } from 'react';

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

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

    window.addEventListener('mousemove', handleMouseMove);

    // Cleanup function to remove the event listener
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Empty dependency array means this effect runs once on mount and cleans up on unmount

  return position;
}

// Usage in a component
function App() {
  const { x, y } = useMousePosition();

  return (
    <div>
      <h1>Move the mouse around!</h1>
      <h1>The mouse position is ({x}, {y})</h1>
    </div>
  );
}

Here, useMousePosition is a custom hook. It encapsulates the state (position) and the effect (useEffect for adding and removing the event listener). The App component simply calls useMousePosition() and gets the current mouse coordinates back. It doesn’t need to know how the coordinates are tracked, just that it can access them. This is the core of hooks: extracting stateful logic into reusable functions.

The primary problem render props solve is code sharing and logic reuse across components without relying on inheritance. Before hooks, if you had stateful logic (like fetching data, managing a subscription, or tracking mouse movement) that you wanted to use in multiple components, render props were a popular solution. The component with the logic (e.g., MouseTracker) would render another component provided by its parent via a prop (the render prop), passing its internal state and methods to that component. This allowed the parent component to control what was rendered while the child component controlled how the logic was executed and what data was available.

Hooks, on the other hand, were introduced to solve the problem of stateful logic reuse and to address limitations of render props and higher-order components (HOCs). Hooks allow you to "hook into" React state and lifecycle features from function components. They make it much cleaner to share stateful logic because you can extract it into a custom hook and then use that hook in any number of function components. The consuming component directly calls the hook and receives the state and functions it needs, without any extra layers of JSX or prop drilling.

The mental model for render props is about "what do you want to render with this data?" The component providing the render prop is a data provider or a behavior provider. It does the work, and then it asks its parent, "Here’s the data/state, what should I show?" The parent component then provides the rendering logic. This pattern is excellent for scenarios where you have a component that does something and you want to give the consumer fine-grained control over the UI that uses that something. Think about libraries that provide UI components with complex internal state (like a data table with sorting and filtering) but allow you to define the row/cell rendering.

The mental model for hooks is about "what stateful logic do you need?" A custom hook is a function that encapsulates a piece of stateful behavior. Components simply import and call these hooks to gain access to that behavior. It’s a more direct and less verbose way to share logic. Hooks are about composing behavior directly within components rather than configuring a component that then renders another component.

Here’s a key difference often missed: render props inherently involve a component rendering another component (the one provided via the prop). This means you’re always dealing with nested component trees. Hooks, however, allow you to extract logic without necessarily creating new component instances or changing the component tree structure. You can have a component that simply calls a hook and directly uses its return values, leading to flatter component trees and potentially better performance because fewer intermediate components are rendered.

When choosing between them, consider the problem. If you have a component that needs to manage some state or behavior and you want to give its parent complete control over the UI that uses that state/behavior, render props can be a good fit. They are particularly useful for libraries that want to expose complex functionality while allowing extensive customization of the presentation. However, for most common logic sharing needs within your own application, hooks are generally preferred due to their conciseness, composability, and directness. Hooks make it easier to extract and reuse stateful logic without the extra indirection of render props.

The next concept you’ll likely encounter when thinking about sharing logic in React is how to abstract complex state management using custom hooks and context.

Want structured learning?

Take the full React course →