Render Props — когда компонент решает, что рендерить
Содержание
- Введение: что такое Render Props
- Базовый пример Render Props
- Альтернативное название —
childrenкак функция - Реальный пример: компонент с загрузкой данных
- Render Props с несколькими пропсами
- Render Props с параметрами
- Композиция Render Props
- Render Props против HOC (Higher Order Components)
- Современная альтернатива — хуки
- Компонент с несколькими render-пропсами
- Render Props в React Router (пример)
- Частые ошибки новичков
- Итог (что мы узнали)
- Задание для закрепления (на 1–2 часа)
- Что дальше?
Цель урока: понять паттерн Render Props — способ передачи функции в качестве пропса, которая определяет, что будет рендериться внутри компонента. Научиться создавать гибкие, переиспользуемые компоненты, которые разделяют логику, но не диктуют отображение.
Введение: что такое Render Props
Render Props — это паттерн, при котором компонент принимает функцию в качестве пропса и вызывает её, возвращая JSX для рендера.
Главная мысль: Render Props — это способ сказать компоненту: «Вот тебе логика, а вот функция, которая решает, как это отобразить».
Почему это важно:
- Разделение логики и отображения
- Максимальная гибкость для пользователя компонента
- Альтернатива HOC (Higher Order Components)
- Особенно полезно для UI-библиотек
Базовый пример Render Props
Компонент MouseTracker с Render Props:
import { useState, useEffect } from 'react';
// Компонент, который следит за позицией мыши
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
// Вызываем функцию render и передаём ей данные
return render(position);
}
// Использование
function App() {
return (
<div>
<h1>Позиция мыши</h1>
<MouseTracker
render={({ x, y }) => (
<div>
<p>X: {x}, Y: {y}</p>
<div
style={{
width: '20px',
height: '20px',
background: 'red',
position: 'absolute',
left: x - 10,
top: y - 10,
borderRadius: '50%'
}}
/>
</div>
)}
/>
</div>
);
}
Что здесь происходит:
MouseTrackerсодержит логику отслеживания мыши- Пользователь передаёт функцию через пропс
render MouseTrackerвызывает эту функцию с данными- Функция возвращает JSX, который будет отрендерен
Альтернативное название — children как функция
Вместо пропса render часто используют children:
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return children(position);
}
// Использование
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<p>Позиция: {x}, {y}</p>
)}
</MouseTracker>
);
}
Что лучше: render или children?
children— более идиоматично для Reactrender— явно указывает на назначение пропса- В современных проектах чаще используют
children
Реальный пример: компонент с загрузкой данных
// Компонент, который загружает данные и передаёт их через render prop
function DataFetcher({ url, children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Ошибка загрузки');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return children({ data, loading, error });
}
// Использование — загрузка пользователей
function UsersPage() {
return (
<DataFetcher url="https://api.example.com/users">
{({ data, loading, error }) => {
if (loading) return <p>Загрузка пользователей...</p>;
if (error) return <p>Ошибка: {error}</p>;
if (!data) return <p>Нет данных</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}}
</DataFetcher>
);
}
// То же самое, но для продуктов
function ProductsPage() {
return (
<DataFetcher url="https://api.example.com/products">
{({ data, loading, error }) => {
if (loading) return <p>Загрузка товаров...</p>;
if (error) return <p>Ошибка: {error}</p>;
if (!data) return <p>Нет товаров</p>;
return (
<div>
{data.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>Цена: {product.price}₽</p>
</div>
))}
</div>
);
}}
</DataFetcher>
);
}
Преимущество: Один компонент — разное отображение!
Оживи свой сайт. Освой JavaScript!
Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.
От теории — к реальным скриптам в твоём портфолио.
Render Props с несколькими пропсами
function WindowSize({ children }) {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return children({
width: size.width,
height: size.height,
isMobile: size.width < 768,
isTablet: size.width >= 768 && size.width < 1024,
isDesktop: size.width >= 1024
});
}
// Использование
function ResponsiveLayout() {
return (
<WindowSize>
{({ width, height, isMobile, isDesktop }) => (
<div>
<p>Ширина: {width}px</p>
<p>Высота: {height}px</p>
{isMobile && <MobileMenu />}
{isDesktop && <DesktopMenu />}
<p>Тип устройства: {
isMobile ? 'Мобильное' :
isDesktop ? 'Десктоп' : 'Планшет'
}</p>
</div>
)}
</WindowSize>
);
}
Render Props с параметрами
Можно передавать параметры внутрь render-функции:
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(prev => !prev);
const setOn = () => setIsOn(true);
const setOff = () => setIsOn(false);
return children({
isOn,
toggle,
setOn,
setOff,
// Вспомогательные функции
getButtonProps: () => ({
onClick: toggle,
'aria-pressed': isOn
})
});
}
// Использование
function ThemeToggle() {
return (
<Toggle>
{({ isOn, toggle, getButtonProps }) => (
<div>
<button {...getButtonProps()}>
{isOn ? '🌙 Тёмная' : '☀️ Светлая'}
</button>
<div style={{
background: isOn ? '#333' : '#fff',
color: isOn ? '#fff' : '#333',
padding: '20px',
marginTop: '10px'
}}>
Тема: {isOn ? 'Тёмная' : 'Светлая'}
</div>
</div>
)}
</Toggle>
);
}
Композиция Render Props
Можно комбинировать несколько компонентов с render props:
function App() {
return (
<WindowSize>
{({ isMobile }) => (
<DataFetcher url={isMobile ? '/api/mobile' : '/api/desktop'}>
{({ data, loading }) => (
<MouseTracker>
{({ x, y }) => (
<div>
<p>Размер: {isMobile ? 'Мобильный' : 'Десктоп'}</p>
<p>Данные: {loading ? 'Загрузка...' : data.length}</p>
<p>Мышь: {x}, {y}</p>
</div>
)}
</MouseTracker>
)}
</DataFetcher>
)}
</WindowSize>
);
}
Важно: Слишком глубокая вложенность может ухудшить читаемость.
Render Props против HOC (Higher Order Components)
| Характеристика | Render Props | HOC |
|---|---|---|
| Синтаксис | Проп-функция | Функция, оборачивающая компонент |
| Передача данных | Через аргументы функции | Через пропсы |
| Гибкость | Высокая (можно выбирать, что рендерить) | Средняя |
| Вложенность | Может быть проблема с «адским» рендером | Можно цепочкой |
| Простота | Интуитивно понятно | Требует понимания HOC-концепции |
| Когда использовать | Когда нужно контролировать рендер | Когда нужно обогатить компонент пропсами |
Современная альтернатива — хуки
В современном React Render Props часто заменяются хуками:
// Хук вместо Render Props
function useMouseTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// Использование
function App() {
const { x, y } = useMouseTracker();
return (
<div>
<p>X: {x}, Y: {y}</p>
<div style={{
width: '20px',
height: '20px',
background: 'red',
position: 'absolute',
left: x - 10,
top: y - 10,
borderRadius: '50%'
}} />
</div>
);
}
Когда использовать Render Props vs Хуки:
- Хуки — в новых проектах (проще и чище)
- Render Props — в старых проектах или при создании UI-библиотек
- Render Props — когда нужно передавать данные не только компоненту, но и его потомкам
Компонент с несколькими render-пропсами
function ComplexLayout({
header,
sidebar,
content,
footer
}) {
const [theme, setTheme] = useState('light');
const [isOpen, setIsOpen] = useState(false);
return (
<div className="layout">
<div className="header">
{header({ theme, toggleTheme: () => setTheme(prev => prev === 'light' ? 'dark' : 'light') })}
</div>
<div className="body">
<div className="sidebar">
{sidebar({ isOpen, toggle: () => setIsOpen(prev => !prev) })}
</div>
<div className="content">
{content({ theme })}
</div>
</div>
<div className="footer">
{footer({ theme })}
</div>
</div>
);
}
// Использование
function App() {
return (
<ComplexLayout
header={({ theme, toggleTheme }) => (
<header>
<h1>Мой сайт</h1>
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
</button>
</header>
)}
sidebar={({ isOpen, toggle }) => (
<nav>
<button onClick={toggle}>☰ Меню</button>
{isOpen && (
<ul>
<li>Главная</li>
<li>О нас</li>
<li>Контакты</li>
</ul>
)}
</nav>
)}
content={({ theme }) => (
<main>
<h2>Основной контент</h2>
<p>Тема: {theme}</p>
</main>
)}
footer={({ theme }) => (
<footer>
<p>© 2025 Все права защищены</p>
<p>Тема: {theme}</p>
</footer>
)}
/>
);
}
Render Props в React Router (пример)
В React Router до версии 6 использовались render props:
import { BrowserRouter, Route } from 'react-router-dom';
<Route
path="/users/:id"
render={({ match }) => {
const userId = match.params.id;
return <UserProfile userId={userId} />;
}}
/>
Сейчас используется element пропс:
<Route path="/users/:id" element={<UserProfile />} />
Частые ошибки новичков
| Ошибка | Почему | Исправление |
|---|---|---|
| Не вызывают функцию рендера | Ничего не отображается | Всегда вызывай children(data) или render(data) |
| Создают функцию внутри render | Лишние перерендеры | Мемоизируй через useCallback |
| Забывают про PropTypes | Нет валидации | Добавь PropTypes для render-функции |
| Передают функцию не в тот пропс | Не работает | Проверь имя пропса (render или children) |
| Вложенные render props | «Ад» из вложений | Выноси в отдельные компоненты |
Итог (что мы узнали)
| Что узнали | Как это работает |
|---|---|
| Render Props | Компонент с функцией для рендера |
children как функция |
Альтернатива render пропсу |
| Передача данных | Через аргументы функции |
| Композиция | Комбинация нескольких render-пропсов |
| Против HOC | Разные подходы к общему коду |
| Против хуков | Хуки — современная альтернатива |
Задание для закрепления (на 1–2 часа)
Создай три компонента с Render Props:
List — компонент, который рендерит список элементов:
- Принимает
items(массив) иrenderItem(функция, принимающая элемент и индекс) - Добавляет разделители между элементами (если передан пропс
withSeparator)
Pagination — компонент для пагинации:
- Принимает
totalItems(всего),pageSize(на странице) - Передаёт в render-функцию:
currentPage,totalPages,goToPage,nextPage,prevPage
Form — компонент управления формой:
- Принимает
initialValuesиonSubmit - Передаёт в render-функцию:
values,handleChange,handleSubmit,errors
Дополнительно: Создай страницу, которая использует все три компонента вместе — список товаров с пагинацией и формой поиска (поиск управляется через Form).
Проверка: Убедись, что все компоненты работают гибко — пользователь сам решает, как отображать данные.
Что дальше?
В следующем уроке разберём High Order Components (HOC) — продвинутый паттерн для обогащения компонентов дополнительной логикой. Ты узнаешь, как создавать функции-обёртки, которые добавляют функциональность (авторизация, логирование, загрузка) без изменения самого компонента.