Шаблон render props у React
Render props у React - це патерн, де компонент приймає функцію як пропс, викликає її зі своїм внутрішнім станом і повертає JSX, який ця функція виробляє.
Теорія
Коротко
- Як вендинговий автомат з дисплейним слотом: автомат відстежує наявність товару (стан), ти визначаєш що показується на дисплеї (render-функція)
- Компонент тримає логіку і стан; колер тримає розмітку
- Render prop може мати будь-яке ім'я, включаючи
childrenколи той використовується як функція - Працює в класових і функціональних компонентах; на відміну від хуків, з'явився до React 16.8
- Для нового коду в функціональних компонентах кастомний хук зазвичай замінює цей патерн
Швидкий приклад
// MouseTracker тримає стан; колер вирішує що рендерити
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>
);
}
// Два різних UI, та сама логіка відстеження
<MouseTracker render={({ x, y }) => <h1>Позиція: {x}, {y}</h1>} />MouseTracker налаштовує слухач подій і оновлює стан. Ти отримуєш координати і вирішуєш що показати. Саме в цьому поділі і є вся ідея.
Чим відрізняється від звичайних children і HOC
Звичайні children - це JSX, React рендерить їх як є. Render prop - це функція, яку ти викликаєш з даними всередині компонента. Функція виконується в замиканні (closure) компонента, тому бачить стан, до якого батьківський компонент не має прямого доступу.
HOC-и огортають компоненти і додають шари до дерева. Render props передають дані у scope колера через виклик функції. Дерево залишається пласким, а потік даних видно прямо в JSX.
Коли використовувати
- Та сама логіка зі станом для різних UI: кожен споживач рендерить незалежно
- Класові компоненти або легасі-код де хуки недоступні
- Розробка бібліотек де споживач повинен мати повний контроль над рендерингом (Downshift, React Virtualized)
- Уникнення стеків HOC-обгорток що захаращують дерево компонентів у DevTools
Для нового функціонального коду useMousePosition() чистіше ніж <MouseTracker render={...} />. Спочатку думай про хуки; до render props коли того вимагає ситуація.
Render props vs кастомні хуки
| Render props | Кастомні хуки | |
|---|---|---|
| Шаринг логіки | Так | Так |
| Додає обгортку до дерева | Так | Ні |
| Працює в класових компонентах | Так | Ні |
| Споживач контролює рендеринг | Так | Ні - повертає дані, ти рендериш |
| Читабельність при вкладеності | Може заглибитися | Чистіше на місці виклику |
Немає однозначно кращого варіанту. Render props виграють коли ти пишеш UI-бібліотеку де потрібно передати контроль над рендерингом споживачу. Хуки - у всьому іншому в сучасному React.
Як React обробляє виклик render prop
React сприймає render пропс як звичайну функцію під час reconciliation. На кожному проході рендерингу він фіксує поточний стан, викликає твою функцію (стандартне замикання над scope компонента) і порівнює повернений JSX з попереднім виведенням. Ніяких спеціальних оптимізацій тут немає.
Пряме наслідування: інлайн стрілочна функція створює новий reference при кожному рендері батьківського компонента. Дочірні компоненти з React.memo побачать новий пропс і перерендеряться навіть якщо дані не змінились. useCallback на render-функції вирішує це.
Типові помилки
Мутація даних переданих у render prop
// Неправильно - пропси незмінні; мутація псує стан батька
<MouseTracker render={pos => { pos.x = 100; return <p>{pos.x}</p>; }} />
// Правильно - розкладаємо в новий об'єкт
<MouseTracker render={pos => { const shifted = { ...pos, x: pos.x + 100 }; return <p>{shifted.x}</p>; }} />Інлайн функція з важким дочірнім компонентом
// Новий reference функції при кожному рендері App
function App() {
return <WindowSize render={size => <Chart data={size} />} />;
}
// Стабілізуємо через useCallback
function App() {
const renderChart = React.useCallback(
(size: { width: number; height: number }) => <Chart data={size} />,
[]
);
return <WindowSize render={renderChart} />;
}Тип children як ReactNode замість функції
// Неправильно - children ніколи не викликається з даними
function Tracker({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
// Правильно
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>
);
}Незв'язаний this у render prop класового компонента
// Неправильно - може кинути помилку або дати застарілі дані
class BadTracker extends React.Component<{ render: (pos: any) => React.ReactNode }> {
render() { return this.props.render(this.state); }
}
// Правильно - використовуємо стрілочне поле класу
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>; }
}Де зустрічається в реальному коді
- React Router v5:
<Route render={({ location }) => <Component loc={location} />} />- API до переходу на хуки - Downshift:
render={({ isOpen, getInputProps }) => ...}- повний контроль над рендерингом автодоповнення передається споживачу - React Virtualized: рендеринг рядків через render prop, щоб колер контролював вигляд кожного рядка
- React Motion: стан анімації передається через render prop; ти вирішуєш що саме анімується
- Formik (ранні версії): використовував цей патерн для рендерингу полів до переходу на хуки
Питання на співбесіді
Q: Яка різниця між render prop і children як функцією?
A: Функціонально те саме. Різниця в зручності: іменований render пропс явний і дозволяє кілька функціональних пропсів на одному компоненті. children як функція синтаксично чистіше для одного слота, але може здивувати розробників які очікують JSX-дітей.
Q: Чим render props відрізняється від HOC?
A: HOC додають компоненти до дерева; render props передають дані через виклик функції. З HOC можна потрапити у пекло обгорток і конфлікти імен пропсів. Render props роблять потік даних видимим прямо в JSX.
Q: Як конвертувати цей патерн у хук?
A: Виносимо стан і ефекти в кастомний хук і повертаємо дані: function useMousePosition() { ... return pos; }. Чистіше на місці виклику, але втрачаємо можливість обгортати поведінку рендерингу всередині компонента.
Q: Чому не замінити render props хуками скрізь?
A: Хуки не працюють в класових компонентах. Автори бібліотек також використовують render props коли хочуть передати повний контроль над рендерингом споживачу, а не просто дані. React Router v5 і Downshift - реальні приклади такого вибору.
Q: Що викликає зайві перерендери з render props і як це виправити?
A: Інлайн render-функція створює новий reference при кожному рендері батька. Якщо дочірній компонент використовує React.memo, він бачить новий пропс і перерендерюється навіть коли дані ідентичні. Стабілізуй функцію через useCallback.
Приклади
Базовий: відстеження позиції миші
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>
);
}
// Той самий трекер, два різних UI - без дублювання логіки
<MouseTracker render={({ x, y }) => <p>Координати: {x}, {y}</p>} />
<MouseTracker render={({ x, y }) => (
<div style={{ position: 'absolute', left: x, top: y }}>Кіт</div>
)} />Обидва споживачі використовують один слухач подій. Render prop замінює візуальне виведення без жодних змін у логіці відстеження.
Середній: завантажувач даних з async-станами
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)}</>;
}
// Дашборд контролює свій UI; завантажувач тримає async-логіку
<UserFetcher userId="42" render={({ user, loading, error }) => {
if (loading) return <div>Завантаження...</div>;
if (error) return <div>Помилка: {error}</div>;
return <div>Привіт, {user.name}!</div>;
}} />UserFetcher тримає async-цикл і переходи стану. Колер вирішує як виглядають loading, помилка і успіх. Я бачив саме такий патерн у кодовій базі де він використовувався для кожного API-запиту до появи хуків, і він досі працює нормально.
Просунутий: розміри вікна з мемоїзованим render prop
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)}</>;
}
// Неправильно: новий reference функції при кожному рендері Dashboard
function Dashboard() {
return <WindowSize render={size => <Chart data={size} />} />;
}
// Правильно: стабільний reference через useCallback
function Dashboard() {
const renderChart = React.useCallback(
(size: { width: number; height: number }) => <Chart data={size} />,
[] // стабільний - залежності не змінюються
);
return <WindowSize render={renderChart} />;
}При зміні розміру вікна WindowSize оновлює стан і викликає render. Зі стабільним reference renderChart, компонент Chart перерендерюється тільки коли вікно справді змінює розмір, а не при кожному непов'язаному рендері Dashboard.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.