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 ifx
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.