Render Props Pattern
Sharing Component Logic Through Functions
LinkedIn Hook
Most React developers hear "render props" and think it is an outdated pattern replaced by hooks. Then an interviewer asks them to explain how React Router's
<Route render={}>, Downshift's<Downshift>, or Formik's<Formik>component works under the hood — and they cannot answer.
Render props did not disappear. They evolved. And understanding this pattern is the key to reading the source code of almost every major React library.
The idea is deceptively simple: instead of a component deciding what to render, you pass it a function that tells it what to render. The component owns the logic — tracking the mouse, managing form state, handling data fetching — and the function you pass decides how that logic gets displayed. One component, infinite visual representations.
Before hooks existed, render props were THE solution for sharing stateful logic between components without inheritance. React Router used them. Apollo Client used them. Downshift, Formik, React Motion — all built on this pattern. And even today, many libraries still expose render prop APIs alongside hooks because render props offer something hooks cannot: inversion of control at the JSX level.
In interviews, they test this pattern in two ways. First, "implement a reusable MouseTracker component using render props." Second, "when would you choose render props over a custom hook?" If you only know hooks, you fail the second question.
In this lesson, I break down the render props pattern from first principles: what problem it solves, how to implement it with both the
renderprop andchildrenas a function, the classic mouse tracker example that every interview references, how it compares to custom hooks, and where you still encounter it in production libraries today.
If you have ever looked at
<Formik>{(formik) => <form>...</form>}</Formik>and wondered why the child is a function — this lesson gives you the complete answer.
Read the full lesson -> [link]
#React #JavaScript #InterviewPrep #Frontend #DesignPatterns #WebDevelopment #CodingInterview #100DaysOfCode
What You'll Learn
- How to pass a render function as a prop to share logic between components
- How to implement the render props pattern using both named props and children as a function
- How to build the classic mouse tracker example that interviews always reference
- How render props compare to custom hooks and when each approach is appropriate
- Where this pattern still appears in major React libraries today
The Concept — Give Me the Data, I Decide How It Looks
Analogy: The Chef and the Plate
Imagine a chef who prepares incredible ingredients — perfectly seared steak, roasted vegetables, a rich sauce. But this chef has no idea how you want your plate arranged. Are you a fine dining restaurant that wants a minimalist presentation? A family diner that piles everything on a big plate? A food photographer who needs each item placed at exact angles?
The chef's solution: "I will prepare the food. You hand me a set of plating instructions, and I will follow them." The chef does the cooking (the logic), and you provide the plating function (the rendering). Different restaurants get the same food with completely different presentations.
This is exactly how render props work. A component handles the stateful logic — tracking mouse position, managing form validation, fetching data — and you pass it a function that receives that data and returns JSX. The component does not know or care what UI you build with its data. It prepares the ingredients. You decide the plate.
The power is in the separation: one logic component, unlimited visual representations. A MouseTracker component tracks the cursor, but whether you display coordinates as text, animate a trailing dot, or highlight a heatmap is entirely up to the function you pass in.
Passing a Render Function as a Prop
The simplest form of render props is passing a function through a named prop — typically called render. The component calls that function with its internal state and renders whatever JSX the function returns.
Code Example 1: Basic Render Prop — Data Fetcher
import { useState, useEffect } from "react";
// This component handles ALL the fetching logic
// It does NOT decide how to display the data — that is the caller's job
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then((res) => res.json())
.then((result) => {
setData(result);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, [url]);
// Call the render function with our state — the caller decides the UI
return render({ data, loading, error });
}
// Usage: Same DataFetcher, two completely different UIs
function UserPage() {
return (
<div>
{/* Display users as a detailed card list */}
<DataFetcher
url="/api/users"
render={({ data, loading, error }) => {
if (loading) return <p>Loading users...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
);
}}
/>
{/* Display the same users as a simple count */}
<DataFetcher
url="/api/users"
render={({ data, loading }) => {
if (loading) return <span>...</span>;
return <span>Total users: {data.length}</span>;
}}
/>
</div>
);
}
// Output when /api/users returns [{ id: 1, name: "Alice", email: "alice@example.com" }]:
//
// First DataFetcher renders:
// - Alice — alice@example.com
//
// Second DataFetcher renders:
// Total users: 1
//
// Same logic component, same data, completely different presentations
Key point: The DataFetcher component is reusable across the entire application. It handles loading states, error handling, and data fetching. Every consumer gets the same reliable data pipeline but renders it however they want.
Children as a Function — The More Common Form
Instead of a named render prop, most libraries use children as a function. This looks cleaner in JSX and is the form you see in Formik, Downshift, and many other libraries.
Code Example 2: The Classic Mouse Tracker
import { useState, useEffect } from "react";
// MouseTracker tracks the mouse position and shares it through children
// This is the example every React interview references
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
// Attach a mousemove listener to track cursor position
function handleMouseMove(event) {
setPosition({ x: event.clientX, y: event.clientY });
}
window.addEventListener("mousemove", handleMouseMove);
// Clean up the listener when the component unmounts
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
// Call children as a function, passing the current mouse position
return children(position);
}
// Usage 1: Display coordinates as text
function CoordinateDisplay() {
return (
<MouseTracker>
{({ x, y }) => (
<p>
Mouse is at ({x}, {y})
</p>
)}
</MouseTracker>
);
}
// Usage 2: A circle that follows the cursor
function FollowingDot() {
return (
<MouseTracker>
{({ x, y }) => (
<div
style={{
position: "fixed",
left: x - 10,
top: y - 10,
width: 20,
height: 20,
borderRadius: "50%",
backgroundColor: "#61dafb",
pointerEvents: "none",
}}
/>
)}
</MouseTracker>
);
}
// Usage 3: A tooltip that appears near the cursor
function CursorTooltip() {
return (
<MouseTracker>
{({ x, y }) => (
<div
style={{
position: "fixed",
left: x + 15,
top: y + 15,
padding: "4px 8px",
background: "#333",
color: "#fff",
borderRadius: 4,
fontSize: 12,
pointerEvents: "none",
}}
>
Hover info here
</div>
)}
</MouseTracker>
);
}
// Output:
// CoordinateDisplay: "Mouse is at (450, 320)" — updates live as mouse moves
// FollowingDot: A blue circle follows the cursor smoothly
// CursorTooltip: A dark tooltip box trails the cursor with an offset
//
// All three use the SAME MouseTracker logic — zero code duplication
Key point: The children prop is just a prop. When you write <MouseTracker>{(pos) => ...}</MouseTracker>, React passes that function as props.children. The component calls children(position) to let the consumer decide the rendering. This is why it is called "children as a function."
Render Props vs Custom Hooks
Custom hooks are the modern way to share stateful logic. So when would you still choose render props?
Code Example 3: The Same Logic as a Hook vs a Render Prop
import { useState, useEffect } from "react";
// APPROACH 1: Custom Hook — the modern way
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMouseMove(event) {
setPosition({ x: event.clientX, y: event.clientY });
}
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
return position;
}
// Using the hook — clean and simple
function DotWithHook() {
const { x, y } = useMouse();
return (
<div
style={{
position: "fixed",
left: x - 5,
top: y - 5,
width: 10,
height: 10,
borderRadius: "50%",
background: "#ff2d55",
}}
/>
);
}
// APPROACH 2: Render Prop — still useful for conditional rendering
function MouseTracker({ children }) {
const { x, y } = useMouse(); // Render prop can USE hooks internally
return children({ x, y });
}
// Using the render prop — useful when you want to CONDITIONALLY render
// based on the shared state WITHOUT creating a new component
function Dashboard() {
const [showTooltip, setShowTooltip] = useState(true);
return (
<div>
<button onClick={() => setShowTooltip((s) => !s)}>
Toggle Tooltip
</button>
{/* With render props, the parent controls what renders */}
{/* No need to create a separate TooltipWithMouse component */}
{showTooltip && (
<MouseTracker>
{({ x, y }) => (
<span style={{ position: "fixed", left: x + 10, top: y + 10 }}>
Tooltip at ({x}, {y})
</span>
)}
</MouseTracker>
)}
</div>
);
}
// When to use which:
//
// Custom Hook:
// - You need the data inside component logic (not just JSX)
// - You want to combine multiple hooks
// - You want cleaner, flatter code
//
// Render Prop:
// - Library needs to support class components (no hooks in classes)
// - You want inversion of control at the JSX level
// - You are building a component that wraps a DOM element (like a Slot pattern)
// - You want to avoid creating many small wrapper components
Key point: Hooks replaced render props for most use cases. But render props still shine when a library needs to support both class and function components, when you want JSX-level composition, or when a component needs to wrap specific DOM structure while letting the consumer control the content.
Where Render Props Appear in Libraries
Code Example 4: Real Library Patterns You Will See
// EXAMPLE 1: Formik — form state management via children as a function
import { Formik } from "formik";
function LoginForm() {
return (
// Formik manages form state, validation, and submission
// You control the entire form UI through the render function
<Formik
initialValues={{ email: "", password: "" }}
onSubmit={(values) => console.log(values)}
>
{({ values, handleChange, handleSubmit, errors }) => (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <span>{errors.email}</span>}
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
/>
<button type="submit">Log In</button>
</form>
)}
</Formik>
);
}
// EXAMPLE 2: React Router v5 — Route with render prop
import { Route } from "react-router-dom";
function App() {
return (
// Route decides WHEN to render (URL match)
// The render prop decides WHAT to render
<Route
path="/profile/:id"
render={({ match, location, history }) => (
<ProfilePage
userId={match.params.id}
goBack={() => history.goBack()}
/>
)}
/>
);
}
// EXAMPLE 3: Downshift — accessible dropdown/autocomplete
import Downshift from "downshift";
const items = ["Apple", "Banana", "Cherry", "Date"];
function Autocomplete() {
return (
// Downshift handles keyboard navigation, ARIA attributes, and selection
// You control every pixel of the dropdown UI
<Downshift>
{({
getInputProps,
getItemProps,
isOpen,
inputValue,
highlightedIndex,
}) => (
<div>
<input {...getInputProps({ placeholder: "Search fruit" })} />
{isOpen && (
<ul>
{items
.filter((item) =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
.map((item, index) => (
<li
key={item}
{...getItemProps({ item, index })}
style={{
background: highlightedIndex === index ? "#61dafb" : "#fff",
}}
>
{item}
</li>
))}
</ul>
)}
</div>
)}
</Downshift>
);
}
// Output (Autocomplete):
// Typing "a" shows: Apple, Banana, Date (all contain "a")
// Arrow keys highlight items, Enter selects, Escape closes
// All accessibility handled by Downshift — you only styled the UI
Key point: These libraries use render props because they manage complex internal logic (form state, route matching, keyboard navigation) while giving you complete control over the visual output. The pattern separates "what the component does" from "what the component looks like."
Common Mistakes
Mistake 1: Creating the render function inside JSX without understanding re-renders
// PROBLEMATIC: The arrow function is recreated on every render of ParentApp
// This means MouseTracker receives a new 'children' prop every time
// If MouseTracker uses React.memo, the memo is defeated
function ParentApp() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
{/* This arrow function is a new reference on every render */}
<MouseTracker>
{({ x, y }) => <p>Position: ({x}, {y})</p>}
</MouseTracker>
</div>
);
}
// BETTER: Extract the render function if you need to prevent re-renders
// But in practice, this only matters if MouseTracker is expensive
function ParentApp() {
const [count, setCount] = useState(0);
// Stable reference — same function across renders
const renderMouse = useCallback(
({ x, y }) => <p>Position: ({x}, {y})</p>,
[]
);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<MouseTracker>{renderMouse}</MouseTracker>
</div>
);
}
// NOTE: This is a micro-optimization. Only do this if profiling shows
// that the render prop component is expensive and re-renders are a problem.
Mistake 2: Nesting render props into callback hell
// BAD: Multiple render props nested inside each other — "render prop hell"
function Dashboard() {
return (
<MouseTracker>
{(mouse) => (
<DataFetcher url="/api/user">
{(user) => (
<ThemeProvider>
{(theme) => (
<div style={{ color: theme.text }}>
<p>User: {user.data?.name}</p>
<p>Mouse: ({mouse.x}, {mouse.y})</p>
</div>
)}
</ThemeProvider>
)}
</DataFetcher>
)}
</MouseTracker>
);
}
// GOOD: Use hooks to flatten the nesting — this is WHY hooks were invented
function Dashboard() {
const mouse = useMouse();
const user = useFetch("/api/user");
const theme = useTheme();
return (
<div style={{ color: theme.text }}>
<p>User: {user.data?.name}</p>
<p>Mouse: ({mouse.x}, {mouse.y})</p>
</div>
);
}
// Hooks solved the nesting problem that was render props' biggest weakness.
// If you find yourself nesting more than one render prop, switch to hooks.
Mistake 3: Forgetting that children is a function, not JSX
// BAD: Passing regular JSX when the component expects children as a function
function BrokenUsage() {
return (
<MouseTracker>
{/* This is JSX, not a function — MouseTracker will crash
when it tries to call children({ x, y }) */}
<p>This will not work</p>
</MouseTracker>
);
// TypeError: children is not a function
}
// GOOD: Always pass a function when the component uses children as a function
function CorrectUsage() {
return (
<MouseTracker>
{({ x, y }) => <p>Mouse at ({x}, {y})</p>}
</MouseTracker>
);
}
// TIP: If you are building a render prop component, add a runtime check:
function MouseTracker({ children }) {
const position = useMouse();
if (typeof children !== "function") {
throw new Error("MouseTracker expects children to be a function");
}
return children(position);
}
Interview Questions
Q: What is the render props pattern and what problem does it solve?
The render props pattern is a technique where a component receives a function (through a prop or children) that returns JSX. The component manages stateful logic internally — mouse tracking, data fetching, form handling — and calls the function with its state so the consumer can decide how to render the UI. It solves the problem of sharing stateful logic between components without duplication or inheritance. Before hooks, this was the primary way to reuse component logic across different visual representations.
Q: What is the difference between using a render prop and children as a function?
They are functionally identical — both pass a function that receives data and returns JSX. A named
renderprop uses<Component render={(data) => <UI />} />, while children as a function uses<Component>{(data) => <UI />}</Component>. The children form is more common in libraries (Formik, Downshift) because it reads more naturally in JSX and avoids the visual noise of a named prop. Under the hood,childrenis just another prop on the props object.
Q: When would you choose render props over custom hooks?
I would choose render props in three situations. First, when building a library that needs to support both class and function components — hooks only work in function components, but render props work everywhere. Second, when the component needs to wrap a specific DOM structure — for example, a dropdown component that renders the trigger button and positions the popup. Third, when I want to provide JSX-level inversion of control where the consumer decides the entire subtree without creating additional wrapper components. For most application code, hooks are simpler and more composable.
Q: Explain the "callback hell" problem with render props. How do hooks solve it?
When you need to combine multiple render prop components, each one nests inside the previous one — MouseTracker wraps DataFetcher wraps ThemeProvider. With three or four levels, the code becomes deeply indented and hard to read, similar to nested callbacks in old JavaScript. Hooks solve this by flattening the logic:
const mouse = useMouse(); const data = useFetch(url); const theme = useTheme();— all at the same indentation level, easy to read and reorder. This nesting problem was one of the key motivations behind the hooks proposal in React 16.8.
Q: Can you name real React libraries that use the render props pattern? How do they use it?
Formik uses children as a function:
<Formik>{({ values, handleChange, handleSubmit }) => <form>...</form>}</Formik>— it manages form state, validation, and submission while you control the form UI. Downshift uses children as a function for accessible dropdown components: it handles keyboard navigation, ARIA attributes, and selection while you render every DOM element. React Router v5 used arenderprop on the Route component:<Route path="/user" render={({ match }) => <User id={match.params.id} />} />. React Spring also offers a render prop API for animations. These libraries chose render props because they manage complex internal state while giving consumers full control over the visual output.
Quick Reference — Cheat Sheet
RENDER PROPS PATTERN
==================================
Core Idea:
A component receives a FUNCTION that returns JSX.
The component calls that function with its internal state.
The consumer decides the visual output.
Two Forms:
Named prop: <Component render={(data) => <UI data={data} />} />
Children func: <Component>{(data) => <UI data={data} />}</Component>
Basic Template:
function Provider({ children }) {
const [state, setState] = useState(initialValue);
// ... logic, effects, event listeners ...
return children(state);
}
// Usage:
<Provider>
{(state) => <div>{state.value}</div>}
</Provider>
Mouse Tracker (Classic Example):
function MouseTracker({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return children(pos);
}
Render Props vs Hooks:
+-------------------+-------------------------+-------------------------+
| Aspect | Render Props | Custom Hooks |
+-------------------+-------------------------+-------------------------+
| Syntax | <C>{(data) => ...}</C> | const data = useHook() |
| Nesting | Can get deeply nested | Flat, composable |
| Class support | Yes | No (functions only) |
| DOM wrapping | Can wrap DOM structure | Returns data only |
| Library API | Common in older libs | Modern standard |
+-------------------+-------------------------+-------------------------+
Libraries Using Render Props:
Formik -> <Formik>{({ values, handleChange }) => ...}</Formik>
Downshift -> <Downshift>{({ getInputProps, isOpen }) => ...}</Downshift>
React Router -> <Route render={({ match }) => ...} /> (v5)
React Spring -> <Spring>{(styles) => ...}</Spring>
Common Mistakes:
1. Nesting multiple render props (use hooks instead)
2. Passing JSX instead of a function to children-as-function components
3. Forgetting that inline render functions create new references each render
Interview Decision Rule:
Need shared logic in application code? -> Custom hook
Building a library API with DOM control? -> Render prop
Supporting class components? -> Render prop
Combining multiple sources of state? -> Hooks (avoid nesting)
Previous: Lesson 9.1 — Higher-Order Components (HOC) -> Next: Lesson 9.3 — Compound Components Pattern ->
This is Lesson 9.2 of the React Interview Prep Course — 10 chapters, 42 lessons.