Что такое render props в React?
Render Props
Render props — это паттерн проектирования в React, при котором компонент принимает проп, значением которого является функция, возвращающая React-элемент. Компонент вызывает эту функцию вместо реализации собственной логики рендеринга, передавая ей своё внутреннее состояние или методы.
Зачем нужен этот паттерн
Основная цель — переиспользование логики с состоянием между разными компонентами без дублирования кода. До появления хуков это был один из главных способов решить проблему совместного использования поведения (наряду с HOC). Паттерн позволяет компоненту-«провайдеру» управлять данными, а потребителю — контролировать отображение.
Как это работает
Компонент с render prop инкапсулирует некую логику (например, отслеживание позиции мыши, статус загрузки, данные формы) и предоставляет её наружу через вызов переданной функции.
// Провайдер: управляет позицией мыши
const MouseTracker = ({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode }) => {
const [pos, setPos] = useState({ x: 0, y: 0 });
return (
<div
style={{ height: '100vh' }}
onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}
>
{render(pos)}
</div>
);
};
// Потребитель: решает, как отображать данные
const App = () => (
<MouseTracker
render={({ x, y }) => <p>Позиция курсора: {x}, {y}</p>}
/>
);
Вариант через children как функцию
Вместо именованного пропа render часто используют children как функцию — это называется «children as a function» и является частным случаем того же паттерна:
const DataFetcher = ({ url, children }: { url: string; children: (data: any, loading: boolean) => React.ReactNode }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json) => {
setData(json);
setLoading(false);
});
}, [url]);
return <>{children(data, loading)}</>;
};
// Использование
const UserProfile = () => (
<DataFetcher url="/api/user">
{(data, loading) => loading ? <Spinner /> : <div>{data?.name}</div>}
</DataFetcher>
);
Render Props vs Hooks
С появлением хуков большинство задач, решаемых через render props, теперь элегантнее решаются через custom hooks:
// Тот же MouseTracker, но как хук
const useMousePosition = () => {
const [pos, setPos] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return pos;
};
Когда render props ещё актуальны
- В библиотеках с широкой поддержкой React < 16.8 (Formik, React Router v5, Downshift)
- Когда логика неотделима от JSX-контекста рендеринга
- Когда нужно явно передавать рендер в слот (compound components, scoped slots)
- В библиотеках компонентов для максимальной гибкости UI
Что хочет услышать интервьюер
Понимание сути паттерна: компонент передаёт своё состояние через вызов функции-пропа
Умение привести рабочий пример кода с render prop или children as a function
Знание главной цели — переиспользование логики с состоянием
Понимание разницы между render props и HOC, и почему хуки вытеснили оба паттерна
Упоминание реальных библиотек, использующих этот паттерн (Formik, React Router, Downshift)
Пример: Базовый пример render prop с отслеживанием мыши
import React, { useState } from 'react';
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
render: (position: MousePosition) => React.ReactNode;
}
// Компонент инкапсулирует логику, делегирует рендеринг
const MouseTracker: React.FC<MouseTrackerProps> = ({ render }) => {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
};
// Потребитель управляет отображением
const App: React.FC = () => (
<MouseTracker
render={({ x, y }) => (
<h2>Позиция курсора: X={x}, Y={y}</h2>
)}
/>
);
export default App;
Пример: Children as a function — загрузка данных
import React, { useState, useEffect } from 'react';
interface FetcherProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: string | null) => React.ReactNode;
}
// Универсальный компонент для загрузки данных
function Fetcher<T>({ url, children }: FetcherProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error('Ошибка сети');
return res.json();
})
.then((json: T) => setData(json))
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [url]);
// Вызываем children как функцию, передавая внутреннее состояние
return <>{children(data, loading, error)}</>;
}
interface User {
id: number;
name: string;
}
const UserList: React.FC = () => (
<Fetcher<User[]> url="/api/users">
{(users, loading, error) => {
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error}</p>;
return (
<ul>
{users?.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}}
</Fetcher>
);
Типичные ошибки
Путают render props с обычной передачей компонента как пропа — не понимают, что ключевое слово «функция, которая вызывается»
Не знают про вариант children as a function и думают, что render prop — это строго проп с именем render
Не могут объяснить проблему производительности: при каждом рендере родителя создаётся новая функция, что мешает PureComponent/memo
Считают паттерн устаревшим и не знают, в каких случаях он до сих пор уместен
Путают render props с HOC, не могут чётко объяснить отличия в структуре и используемых компонентах


