Learnitweb

Problems of Using Render Props with Pure Components in Functional React

React has evolved significantly over the years—from class-based components to a more functional, declarative paradigm. Along with this evolution, new patterns and optimization techniques have emerged. One such pattern is render props, used for reusing component logic. Another is React.memo, used to prevent unnecessary renders by performing shallow comparison of props in functional components.

However, using render props with React.memo (pure components) can lead to performance issues if not handled correctly. In this article, we’ll explore the pitfalls of combining these patterns and how to solve them.

What Are Render Props?

Render props is a pattern where a component receives a function as a prop and calls it to determine what to render.

const MouseTracker = ({ render }) => {
  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 <div>{render(position)}</div>;
};

You can use this component like so:

<MouseTracker render={(pos) => <h1>Mouse at {pos.x}, {pos.y}</h1>} />

What is React.memo?

React.memo is a higher-order component that memoizes a functional component. It re-renders the component only if the props change (shallow comparison).

const DisplayX = React.memo(({ x }) => {
  console.log("DisplayX rendered");
  return <h2>X: {x}</h2>;
});

The Problem: Function References Change on Every Render

Let’s combine the two:

const App = () => {
  return (
    <MouseTracker render={(pos) => <DisplayX x={pos.x} />} />
  );
};

Issue:

The render prop function (pos) => <DisplayX x={pos.x} /> is re-created on every render. Since it’s a new function reference, MouseTracker sees the render prop as changed, and React re-renders it. As a result, DisplayX also re-renders, even if pos.x hasn’t changed.

This behavior breaks the optimization React.memo is supposed to provide.

Demonstration

const DisplayX = React.memo(({ x }) => {
  console.log("Rendering DisplayX");
  return <div>X: {x}</div>;
});

const App = () => {
  return (
    <MouseTracker render={(pos) => <DisplayX x={pos.x} />} />
  );
};
  • The app works.
  • But DisplayX renders every time the mouse moves, even if x didn’t change.
  • The render function changes every render, breaking memoization.

Solution 1: Use useCallback to Stabilize the Function

const App = () => {
  const renderMouse = useCallback(
    (pos) => <DisplayX x={pos.x} />,
    [] // empty dependency array keeps the function stable
  );

  return <MouseTracker render={renderMouse} />;
};

Now the function reference is stable, and React.memo works properly. DisplayX re-renders only when x actually changes.

Solution 2: Prefer Custom Hooks Over Render Props

Render props were widely used before hooks were introduced. In modern React, the idiomatic way to reuse logic is to use a custom hook.

Convert the logic into a hook:

const 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;
};

Use the hook directly inside the component:

const DisplayX = React.memo(() => {
  const { x } = useMousePosition();
  console.log("DisplayX rendered");
  return <h2>X: {x}</h2>;
});

const App = () => {
  return <DisplayX />;
};

Now:

  • No render prop is needed
  • No function reference issues
  • DisplayX is cleanly memoized

Summary

While render props were once a powerful pattern in React, they can clash with modern optimization strategies like React.memo due to function reference instability.

When using render props:

  • Use useCallback to ensure stable function references.
  • Be cautious when using them with pure/memoized components.

Whenever possible, prefer custom hooks for cleaner, more performant, and idiomatic code in functional React.