Suggest an editImprove this articleRefine the answer for “Render props pattern in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Render props pattern** is a technique where a component calls a function prop with its internal state, letting the caller decide what to render. ```tsx <MouseTracker render={({ x, y }) => <p>X: {x}, Y: {y}</p>} /> ``` **Key point:** since React 16.8, custom hooks cover most render prop use cases, but the pattern stays relevant in class components and UI libraries like Downshift and React Virtualized.Shown above the full answer for quick recall.Answer (EN)Image**Render props pattern** is a technique where a component accepts a function as a prop, calls it with its own internal state or data, and returns whatever JSX that function produces. ## Theory ### TL;DR - Like a vending machine with a display slot: the machine tracks inventory (state), you supply what shows in the slot (the render function) - The component owns logic and state; the caller owns the markup - A render prop can be any prop name, including `children` when used as a function - Works in class and function components; unlike hooks, it predates React 16.8 - For new functional code, a custom hook usually replaces the pattern cleanly ### Quick example ```tsx // MouseTracker owns state; the caller decides what to render function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode; }) { const [pos, setPos] = React.useState({ x: 0, y: 0 }); return ( <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })} style={{ height: '100vh' }}> {render(pos)} {/* Calls your function with current position */} </div> ); } // Two UIs, same tracking logic - no duplication <MouseTracker render={({ x, y }) => <h1>Position: {x}, {y}</h1>} /> ``` `MouseTracker` handles the event listener and state update. You get the coordinates and decide what to show. That division is the whole point. ### How it differs from plain children and HOCs Regular `children` is JSX - React renders it as-is. A render prop is a **function** you call with data inside the component. The function runs in the component's closure, so it has access to state the parent never touches directly. HOCs wrap components and stack layers in the tree. Render props inject data into the caller's scope via a function call. The tree stays flatter and the data flow is explicit in JSX. ### When to use - Same stateful logic, different UIs: render props let each consumer render independently - Class components or legacy codebases where hooks are not available - Building libraries where consumers need full rendering control (Downshift, React Virtualized) - Avoiding HOC wrapper stacks that obscure your component tree in DevTools For new code in function components, `useMousePosition()` is cleaner than `<MouseTracker render={...} />`. Reach for hooks first; render props when the situation calls for them. ### Render props vs custom hooks | | Render props | Custom hooks | |---|---|---| | Shares logic | Yes | Yes | | Adds a wrapper to the tree | Yes | No | | Works in class components | Yes | No | | Consumer controls rendering | Yes | No - returns data, you render | | Readability with nesting | Can get deep | Cleaner at call site | Neither is always better. Render props win when you ship a UI library that needs caller control over rendering. Hooks win for everything else in modern React. ### How React handles the render prop call React treats the `render` prop as a plain function during reconciliation. On each render pass, it captures current state, calls your function (a standard closure over component scope), and diffs the returned JSX against the previous output. No special optimization runs here. One direct consequence: an inline arrow function creates a new reference on every parent render. Child components using `React.memo` will re-render because they see a new prop, even when data has not changed. `useCallback` on the render function fixes this. ### Common mistakes **Mutating the data passed to the render prop** ```tsx // Wrong - props are immutable; this corrupts parent state <MouseTracker render={pos => { pos.x = 100; return <p>{pos.x}</p>; }} /> // Right - spread into a new object <MouseTracker render={pos => { const shifted = { ...pos, x: pos.x + 100 }; return <p>{shifted.x}</p>; }} /> ``` **Inline function with an expensive child** ```tsx // Creates a new fn reference every time App re-renders function App() { return <WindowSize render={size => <Chart data={size} />} />; } // Stabilize with useCallback function App() { const renderChart = React.useCallback( (size: { width: number; height: number }) => <Chart data={size} />, [] ); return <WindowSize render={renderChart} />; } ``` **Typing `children` as `ReactNode` when it should be a function** ```tsx // Wrong - children is never called with data function Tracker({ children }: { children: React.ReactNode }) { return <div>{children}</div>; } // Right function Tracker({ children }: { children: (pos: { x: number; y: number }) => React.ReactNode; }) { const [pos, setPos] = React.useState({ x: 0, y: 0 }); return ( <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })}> {children(pos)} </div> ); } ``` **Unbound `this` in class component render props** ```tsx // Wrong - may throw or produce stale data class BadTracker extends React.Component<{ render: (pos: any) => React.ReactNode }> { render() { return this.props.render(this.state); } } // Right - use an arrow class field class GoodTracker extends React.Component<{ render: (pos: any) => React.ReactNode }> { state = { x: 0, y: 0 }; handleMove = (e: React.MouseEvent) => this.setState({ x: e.clientX, y: e.clientY }); render() { return <div onMouseMove={this.handleMove}>{this.props.render(this.state)}</div>; } } ``` ### Real-world usage - **React Router v5**: `<Route render={({ location }) => <Component loc={location} />} />` - the pre-hooks API for accessing route data - **Downshift**: `render={({ isOpen, getInputProps }) => ...}` - full autocomplete rendering handed to the consumer - **React Virtualized**: row rendering via render prop so callers control how each row looks - **React Motion**: animation state exposed via render prop; you decide what the animated element is - **Formik** (early versions): used this pattern for field rendering before moving to hooks ### Follow-up questions **Q:** What is the difference between a render prop and `children` as a function? **A:** Functionally the same. The difference is ergonomics: a named `render` prop is explicit and allows multiple function props on one component. `children` as a function is syntactically cleaner for single-slot cases but can surprise developers who expect JSX children. **Q:** How does render props compare to HOCs? **A:** HOCs add components to the tree; render props inject data via a function call. With HOCs you can hit wrapper hell and prop name collisions. Render props make the data flow visible directly in JSX. **Q:** How would you convert this pattern to a hook? **A:** Extract the state and effects into a custom hook and return the data: `function useMousePosition() { ... return pos; }`. Cleaner at the call site, but you lose the ability to wrap rendering behavior inside a component. **Q:** Why not replace render props with hooks everywhere? **A:** Hooks don't work in class components. Library authors also use render props when they want to give the consumer full rendering control, not just data. React Router v5 and Downshift are real examples of this choice. **Q:** What causes unnecessary re-renders with render props, and how do you fix it? **A:** An inline render function creates a new function reference on every parent render. If the child uses `React.memo`, it sees a new prop and re-renders even when the actual data is identical. Stabilize the function with `useCallback`. ## Examples ### Basic: mouse position tracker ```tsx import React from 'react'; function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode; }) { const [pos, setPos] = React.useState({ x: 0, y: 0 }); return ( <div onMouseMove={e => setPos({ x: e.clientX, y: e.clientY })} style={{ height: '100vh' }} > {render(pos)} </div> ); } // Same tracker, two different UIs - no logic duplication <MouseTracker render={({ x, y }) => <p>Coordinates: {x}, {y}</p>} /> <MouseTracker render={({ x, y }) => ( <div style={{ position: 'absolute', left: x, top: y }}>Cat</div> )} /> ``` Both consumers share one event listener setup. The render prop swaps out the visual output without touching the tracking logic. ### Intermediate: data fetcher with async states ```tsx function UserFetcher({ userId, render }: { userId: string; render: (state: { user: any | null; loading: boolean; error: string | null }) => React.ReactNode; }) { const [state, setState] = React.useState({ user: null, loading: true, error: null }); React.useEffect(() => { setState({ user: null, loading: true, error: null }); fetch(`/api/user/${userId}`) .then(res => res.json()) .then(user => setState({ user, loading: false, error: null })) .catch(err => setState({ user: null, loading: false, error: err.message })); }, [userId]); return <>{render(state)}</>; } // Dashboard controls its own UI, fetcher handles async lifecycle <UserFetcher userId="42" render={({ user, loading, error }) => { if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return <div>Hello, {user.name}!</div>; }} /> ``` `UserFetcher` owns the async lifecycle and state transitions. The caller decides how loading, error, and success look. I've seen this exact pattern used for every API call in pre-hooks codebases, and it holds up well even now. ### Advanced: window size with memoized render prop ```tsx function WindowSize({ render }: { render: (size: { width: number; height: number }) => React.ReactNode; }) { const [size, setSize] = React.useState({ width: window.innerWidth, height: window.innerHeight, }); React.useEffect(() => { const update = () => setSize({ width: window.innerWidth, height: window.innerHeight }); window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); return <>{render(size)}</>; } // Wrong: new function reference on every Dashboard render function Dashboard() { return <WindowSize render={size => <Chart data={size} />} />; } // Right: stable reference with useCallback function Dashboard() { const renderChart = React.useCallback( (size: { width: number; height: number }) => <Chart data={size} />, [] // stable - no deps change ); return <WindowSize render={renderChart} />; } ``` On resize, `WindowSize` updates state and calls `render`. With a stable `renderChart` reference, `Chart` only re-renders when the window actually changes size, not on every unrelated `Dashboard` re-render.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.