Render Props Pattern
Reviewed & published by Brayan K
A render prop is a prop whose value is a function that returns JSX, letting one component own some state or behavior while the parent decides exactly what to render with it — most often written as a function passed as children. It was the classic way to share reusable logic before hooks arrived.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- What a render prop is and how the function gets called
- The 'children as a function' idiomatic form
- Sharing stateful logic across different UIs
- How render props compare with custom hooks
- Why hooks usually replace render props today
- When render props are still the right tool
1️⃣ A Prop That Is a Function Returning JSX
The component holds the logic — say, tracking the mouse — and accepts a render prop. Instead of rendering fixed markup, it calls props.render(data) and returns whatever the parent's function produces. The same component can power totally different UIs.
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
// ...update pos on mousemove...
return render(pos); // parent decides the markup
}
// Two different renderings of the SAME logic:
<MouseTracker render={(p) => <p>At {p.x}, {p.y}</p>} />
<MouseTracker render={(p) => <Dot x={p.x} y={p.y} />} />// A render prop is just a FUNCTION you pass in that returns the UI.
// The component owns the data and calls your function with it.
// Here, getData simulates the component's logic, and render is
// the function the parent supplies.
function withData(render) {
const data = { count: 3, label: "items" }; // component's own state
return render(data); // hand data to the parent's fn
}
// Parent decides the output by passing a render function:
const text = withData((d) => d.count + " " + d.label);
console.log(text); // 3 items
const emoji = withData((d) => "🛒 " + d.count);
console.log(emoji); // 🛒 3 (same logic, different rendering)2️⃣ Children as a Function
The idiomatic version passes the function as children instead of a named prop, and the component calls children(data). It reads more naturally because the function sits right between the tags, exactly where children normally go.
function Mouse({ children }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
return children(pos); // call the function child
}
// Usage — the child IS a function:
<Mouse>
{(pos) => <p>x: {pos.x}, y: {pos.y}</p>}
</Mouse>// The idiomatic form: pass the function as CHILDREN.
// The component calls children(data). Same idea, cleaner JSX.
function Mouse({ children }) {
const position = { x: 10, y: 20 }; // component tracks this for real
return children(position); // call the function child
}
// "Render" by passing a function child:
const out = Mouse({ children: (pos) => "x=" + pos.x + ", y=" + pos.y });
console.log(out); // x=10, y=20
// In JSX this reads as:
// <Mouse>{(pos) => <p>x={pos.x}</p>}</Mouse>3️⃣ Render Props vs Custom Hooks
A custom hook like useMousePosition() shares the same logic with no extra nesting — that's why hooks usually win today. Render props still shine when the shared thing must control rendering (e.g. a virtualized list deciding which rows to draw) or in libraries supporting class components. Watch for "wrapper hell" when you nest several.
// Render prop (extra nesting):
<Mouse>{(pos) => <Battery>{(level) => <p>{pos.x} {level}</p>}</Battery>}</Mouse>
// Custom hook (flat — usually preferred):
function App() {
const pos = useMousePosition();
const level = useBattery();
return <p>{pos.x} {level}</p>;
}// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// Toggle owns "on" state and exposes a toggle() function.
// CALL the render function with { on, toggle } so the parent can render.
function Toggle(render) {
let on = false;
const toggle = () => { on = !on; };
return ___({ on, toggle }); // 👉 call render with the state object
}
const label = Toggle((api) => (api.on ___ "ON" : "OFF")); // 👉 use ? for ternary
console.log(label); // OFF
const label2 = Toggle((api) => { api.toggle(); return api.on ? "ON" : "OFF"; });
console.log(label2); // OFF (toggle ran AFTER we captured on, so still OFF)
// ✅ Expected output:
// OFF
// OFF🧩 Reorder Challenge
These lines build a Toggle component that uses a children-as-function. Put them in the right order:
A. const [on, setOn] = useState(false);
B. function Toggle({ children }) {
C. const toggle = () => setOn((o) => !o);
D. return children({ on, toggle });
E. // usage: <Toggle>{({ on }) => <p>{on ? "ON" : "OFF"}</p>}</Toggle>Common Errors (and the fix)
- "children is not a function". — You used <Mouse><p/></Mouse> instead of a function child. Pass {(data) => <p/>}.
- Rendering nothing. — Your render function forgot to return the JSX. Make sure it returns an element.
- Wrapper hell. — Deeply nested render props are unreadable. If you only need shared logic, switch to a custom hook.
- New function every render hurting memoized children. — The inline function changes identity each render. Pull it out or accept the trade-off; often a hook is cleaner.
📋 Quick Reference
| Form | Shape |
|---|---|
| Named render prop | <C render={(d) => <UI/>} /> |
| Children as function | <C>{(d) => <UI/>}</C> |
| Inside the component | return children(data) |
| Modern alternative | const d = useThing() |
🧠 Quick Recall
1. What type of value is a render prop?
A function that returns JSX. The component calls it with its data and renders the result.
2. What does "children as a function" mean?
You pass a function as children, and the component calls children(data) — the idiomatic render-prop form.
3. Why do hooks usually replace render props?
A custom hook shares the same logic without extra nesting or wrapper hell, keeping the JSX flat.
Mini-Challenge: A Counter Render-Prop
Build a Counter that owns its count and an increment function, then hands them to a render function the caller supplies. Run it and check your output.
// 🎯 MINI-CHALLENGE: a Counter render-prop
//
// Build Counter(render) that owns a count (start at 0) and an
// increment() function, then RETURNS render({ count, increment }).
// Call it twice: once rendering "count: 0", once where you increment
// first then render the new count.
//
// ✅ Expected output:
// count: 0
// count: 1
// your code here🎉 Lesson Complete
- ✅ A render prop is a function-valued prop returning JSX
- ✅ The children-as-function form is the idiomatic version
- ✅ It shares stateful logic while the parent controls rendering
- ✅ Custom hooks usually replace it and avoid wrapper hell
Practice quiz
What is a render prop?
- A prop that holds a string of HTML
- A built-in React hook
- A prop whose value is a function that returns JSX
- A CSS property
Answer: A prop whose value is a function that returns JSX. A render prop is a function-valued prop the component calls with its data to produce the UI.
Who decides the markup when using a render prop?
- The parent, via the function it passes in
- The component that owns the logic
- React automatically
- The render prop never affects markup
Answer: The parent, via the function it passes in. The component owns the state/behavior, but the parent's render function decides what to render.
What is the idiomatic 'children as a function' form?
- <Mouse><p/></Mouse>
- <Mouse render />
- useMouse()
- <Mouse>{(data) => <p>{data.x}</p>}</Mouse>
Answer: <Mouse>{(data) => <p>{data.x}</p>}</Mouse>. You pass a function as children, and the component calls children(data).
Inside a render-prop component, how is the function invoked?
- return children;
- return children(data);
- return <children />;
- return render;
Answer: return children(data);. The component calls the function with its data, e.g. return children(pos).
What mainly replaces render props in modern React for sharing logic?
- Custom hooks
- Higher-order components
- Context only
- Class components
Answer: Custom hooks. A custom hook shares the same logic with no extra nesting, so hooks usually win today.
What is 'wrapper hell' in the render-props pattern?
- Too many props on one component
- A memory leak
- Deeply nested, pyramid-shaped JSX from nesting several render-prop components
- An infinite render loop
Answer: Deeply nested, pyramid-shaped JSX from nesting several render-prop components. Nesting multiple render-prop components produces hard-to-read, deeply indented JSX; hooks flatten it.
When are render props still a good fit?
- For all new code
- When the shared thing must control rendering, or in libraries targeting class components
- Never
- Only for styling
Answer: When the shared thing must control rendering, or in libraries targeting class components. They shine when the shared logic must control what's rendered (e.g. a virtualized list deciding rows).
Why does '<Mouse><p/></Mouse>' throw 'children is not a function'?
- Mouse is missing a key
- p is not a valid tag
- Mouse needs a ref
- children must be a function, but a JSX element was passed
Answer: children must be a function, but a JSX element was passed. The component calls children(data); passing an element instead of a function breaks that call.
If a render-prop component renders nothing, a likely cause is…
- The component is memoized
- The function returned undefined (forgot to return JSX)
- Too many props
- Missing a key
Answer: The function returned undefined (forgot to return JSX). If your render function forgets to return the JSX, nothing renders.
A downside of passing an inline render function each render is…
- It can't access data
- It throws an error
- Its identity changes every render, which can hurt memoized children
- It disables state
Answer: Its identity changes every render, which can hurt memoized children. A new inline function each render changes identity; pull it out or accept the trade-off (often a hook is cleaner).
Continue this course
- Previous: Compound Components Pattern
- Next: Higher-Order Components (HOCs) — Wrap a component to add behavior — withAuth, withLogging, and prop forwarding
- Quick reference: React cheat sheet
Frequently asked questions
What exactly is a render prop?
A render prop is a prop whose value is a function that returns JSX. The component owns some state or behavior, then calls that function with the data and renders whatever it returns — letting the parent decide the markup while the component handles the logic.
Is 'children as a function' the same thing?
Yes, it's the most common form. Instead of a prop literally named render, you pass a function as children: <Mouse>{(pos) => <p>{pos.x}</p>}</Mouse>. The component calls children(data). It reads cleanly and is the idiomatic version.
Aren't custom hooks the modern replacement?
For sharing logic, yes — a custom hook like useMousePosition is usually simpler and avoids extra nesting. Render props predate hooks. You'd still reach for them when the shared thing must control rendering, or in libraries that target class components.
What is the 'wrapper hell' downside?
Nesting several render-prop components produces deeply indented, pyramid-shaped JSX that's hard to read. Hooks flatten this. If you only need shared state or logic (not shared rendering), prefer a hook.