Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Шаблон render props у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Render props** у React - це патерн де компонент викликає функцію-пропс зі своїм внутрішнім станом, а колер вирішує що рендерити. ```tsx <MouseTracker render={({ x, y }) => <p>X: {x}, Y: {y}</p>} /> ``` **Ключове:** після React 16.8 кастомні хуки покривають більшість випадків, але патерн лишається актуальним у класових компонентах і бібліотеках типу Downshift та React Virtualized.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Render props** у React - це патерн, де компонент приймає функцію як пропс, викликає її зі своїм внутрішнім станом і повертає JSX, який ця функція виробляє. ## Теорія ### Коротко - Як вендинговий автомат з дисплейним слотом: автомат відстежує наявність товару (стан), ти визначаєш що показується на дисплеї (render-функція) - Компонент тримає логіку і стан; колер тримає розмітку - Render prop може мати будь-яке ім'я, включаючи `children` коли той використовується як функція - Працює в класових і функціональних компонентах; на відміну від хуків, з'явився до React 16.8 - Для нового коду в функціональних компонентах кастомний хук зазвичай замінює цей патерн ### Швидкий приклад ```tsx // 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** ```tsx // Неправильно - пропси незмінні; мутація псує стан батька <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>; }} /> ``` **Інлайн функція з важким дочірнім компонентом** ```tsx // Новий 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` замість функції** ```tsx // Неправильно - 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 класового компонента** ```tsx // Неправильно - може кинути помилку або дати застарілі дані 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`. ## Приклади ### Базовий: відстеження позиції миші ```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> ); } // Той самий трекер, два різних 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-станами ```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)}</>; } // Дашборд контролює свій 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 ```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)}</>; } // Неправильно: новий 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`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.