Render Props — паттерн, при котором компонент принимает функцию как проп (чаще всего children или render). Эта функция вызывается с данными/состоянием и возвращает JSX:
// Паттерн render prop: children — это функция
<DataProvider>
{(data) => <UserCard user={data.user} />}
</DataProvider>
// Или через проп render:
<MouseTracker render={({ x, y }) => <Cursor x={x} y={y} />} />Суть паттерна: компонент управляет состоянием, вы управляете отрисовкой.
class MouseTracker extends React.Component {
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({ x: event.clientX, y: event.clientY })
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{/* Вызываем children как функцию с данными состояния */}
{this.props.children(this.state)}
</div>
)
}
}
// Использование: разные компоненты могут использовать одну логику
<MouseTracker>
{({ x, y }) => <p>Мышь: {x}, {y}</p>}
</MouseTracker>
<MouseTracker>
{({ x, y }) => <Tooltip style={{ left: x, top: y }}>Подсказка</Tooltip>}
</MouseTracker>Самый практичный пример — компонент для загрузки данных:
function DataFetcher({ url, children }) {
const [state, setState] = useState({ data: null, isLoading: true, error: null })
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(data => setState({ data, isLoading: false, error: null }))
.catch(err => setState({ data: null, isLoading: false, error: err.message }))
}, [url])
// Передаём всё состояние в children-функцию
return children(state)
}
// Разные компоненты — одна логика загрузки:
<DataFetcher url="/api/users">
{({ data, isLoading, error }) => {
if (isLoading) return <Spinner />
if (error) return <ErrorMessage text={error} />
return <UserList users={data} />
}}
</DataFetcher>
<DataFetcher url="/api/products">
{({ data, isLoading }) =>
isLoading ? <Skeleton /> : <ProductGrid items={data} />
}
</DataFetcher>function Toggle({ children, initialOn = false }) {
const [on, setOn] = useState(initialOn)
const toggle = () => setOn(prev => !prev)
const turnOn = () => setOn(true)
const turnOff = () => setOn(false)
// Передаём API управления состоянием
return children({ on, toggle, turnOn, turnOff })
}
// Использование 1: простая кнопка
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>{on ? 'ВКЛ' : 'ВЫКЛ'}</button>
)}
</Toggle>
// Использование 2: сложный UI с тем же состоянием
<Toggle initialOn={true}>
{({ on, toggle, turnOff }) => (
<div>
<span>Тёмная тема: {on ? 'да' : 'нет'}</span>
<button onClick={toggle}>Переключить</button>
<button onClick={turnOff}>Сбросить</button>
</div>
)}
</Toggle>// Один сценарий — три реализации:
// 1. HOC:
const ToggleButton = withToggle(Button)
// Минус: пропсы приходят магически, непрозрачно
// 2. Render Props:
<Toggle>{({ on, toggle }) => <Button on={on} onClick={toggle} />}</Toggle>
// Плюс: явно видно что передаётся; Минус: вложенность
// 3. Кастомный хук (предпочтительно сейчас):
function ToggleButton() {
const { on, toggle } = useToggle()
return <Button on={on} onClick={toggle} />
}1. Библиотеки форм — Formik использует render props: <Field>{({ field }) => <input {...field} />}</Field>
2. Передача нескольких значений — удобно читается без деструктуризации
3. Динамическое управление рендером — когда нужно полностью контролировать что рисуется
4. Несколько "слотов" — компонент с несколькими функциями-пропсами
<DataGrid
data={items}
renderHeader={() => <thead>...</thead>}
renderRow={(item) => <tr key={item.id}>{item.name}</tr>}
renderEmpty={() => <p>Нет данных</p>}
/>Реализация паттерна Render Props через функции JavaScript: DataProvider с children-функцией, Toggle компонент, и DataFetcher с состояниями загрузки
// Реализуем паттерн Render Props через обычные функции.
// В React children — функция. Здесь — любой callback.
// --- Компонент 1: Toggle (управляет булевым состоянием) ---
function createToggle(initialOn = false) {
let on = initialOn
const listeners = []
function notify() {
listeners.forEach(fn => fn({ on, toggle, turnOn, turnOff }))
}
function toggle() {
on = !on
notify()
}
function turnOn() { on = true; notify() }
function turnOff() { on = false; notify() }
// "render" = вызов children-функции с текущим состоянием
function render(childrenFn) {
return childrenFn({ on, toggle, turnOn, turnOff })
}
// subscribe = аналог useEffect для реактивности
function subscribe(fn) { listeners.push(fn) }
return { render, toggle, turnOn, turnOff, subscribe }
}
console.log('=== Toggle с render prop ===')
const toggle1 = createToggle(false)
// "Рендерим" разные UI с одним состоянием
const ui1 = toggle1.render(({ on, toggle }) =>
'Кнопка: [' + (on ? '● ВКЛ' : '○ ВЫКЛ') + '] (click=toggle)'
)
console.log(ui1) // [○ ВЫКЛ]
toggle1.toggle()
const ui2 = toggle1.render(({ on, turnOff }) =>
'Чекбокс: ' + (on ? '☑' : '☐') + ' Тёмная тема | Кнопка [Сбросить]'
)
console.log(ui2) // ☑ Тёмная тема
// --- Компонент 2: DataFetcher (управляет загрузкой данных) ---
function createDataFetcher(fetchFn) {
let state = { data: null, isLoading: false, error: null }
async function load(params) {
state = { data: null, isLoading: true, error: null }
try {
const data = await fetchFn(params)
state = { data, isLoading: false, error: null }
} catch (err) {
state = { data: null, isLoading: false, error: err.message }
}
return state
}
// render prop — вызывается с текущим состоянием
function render(childrenFn) {
return childrenFn(state)
}
return { load, render }
}
// Использование 1: список пользователей
async function testDataFetcher() {
const fakeApi = (endpoint) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (endpoint === '/users') {
resolve([
{ id: 1, name: 'Алексей' },
{ id: 2, name: 'Мария' },
])
} else {
reject(new Error('404 Not Found'))
}
}, 50)
})
}
const fetcher1 = createDataFetcher(fakeApi)
console.log('
=== DataFetcher: состояния загрузки ===')
// До загрузки
await fetcher1.load('/users')
const result = fetcher1.render(({ data, isLoading, error }) => {
if (isLoading) return '[Загрузка...]'
if (error) return '[Ошибка: ' + error + ']'
return '[Список: ' + data.map(u => u.name).join(', ') + ']'
})
console.log('Результат:', result) // [Список: Алексей, Мария]
// Ошибка
const fetcher2 = createDataFetcher(fakeApi)
await fetcher2.load('/nonexistent')
const errorResult = fetcher2.render(({ error }) =>
error ? '[Ошибка: ' + error + ']' : '[Данные]'
)
console.log('Ошибка:', errorResult)
}
// --- Компонент 3: SortableList (render props для строк) ---
function createSortableList(items) {
let sortedItems = [...items]
let sortKey = null
let sortDir = 'asc'
function sortBy(key) {
if (sortKey === key) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc'
} else {
sortKey = key
sortDir = 'asc'
}
sortedItems = [...items].sort((a, b) => {
const val = sortDir === 'asc'
? String(a[key]).localeCompare(String(b[key]))
: String(b[key]).localeCompare(String(a[key]))
return val
})
}
// render prop: renderRow вызывается для каждого элемента
function render(renderRow) {
return sortedItems.map(renderRow)
}
return { sortBy, render }
}
console.log('
=== SortableList с render prop ===')
const list = createSortableList([
{ name: 'Мария', city: 'Москва' },
{ name: 'Алексей', city: 'СПб' },
{ name: 'Ирина', city: 'Казань' },
])
console.log('Исходный порядок:')
list.render(item => console.log(' -', item.name, '|', item.city))
list.sortBy('name')
console.log('
Сортировка по имени:')
list.render(item => console.log(' -', item.name, '|', item.city))
testDataFetcher()Render Props — паттерн, при котором компонент принимает функцию как проп (чаще всего children или render). Эта функция вызывается с данными/состоянием и возвращает JSX:
// Паттерн render prop: children — это функция
<DataProvider>
{(data) => <UserCard user={data.user} />}
</DataProvider>
// Или через проп render:
<MouseTracker render={({ x, y }) => <Cursor x={x} y={y} />} />Суть паттерна: компонент управляет состоянием, вы управляете отрисовкой.
class MouseTracker extends React.Component {
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({ x: event.clientX, y: event.clientY })
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{/* Вызываем children как функцию с данными состояния */}
{this.props.children(this.state)}
</div>
)
}
}
// Использование: разные компоненты могут использовать одну логику
<MouseTracker>
{({ x, y }) => <p>Мышь: {x}, {y}</p>}
</MouseTracker>
<MouseTracker>
{({ x, y }) => <Tooltip style={{ left: x, top: y }}>Подсказка</Tooltip>}
</MouseTracker>Самый практичный пример — компонент для загрузки данных:
function DataFetcher({ url, children }) {
const [state, setState] = useState({ data: null, isLoading: true, error: null })
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(data => setState({ data, isLoading: false, error: null }))
.catch(err => setState({ data: null, isLoading: false, error: err.message }))
}, [url])
// Передаём всё состояние в children-функцию
return children(state)
}
// Разные компоненты — одна логика загрузки:
<DataFetcher url="/api/users">
{({ data, isLoading, error }) => {
if (isLoading) return <Spinner />
if (error) return <ErrorMessage text={error} />
return <UserList users={data} />
}}
</DataFetcher>
<DataFetcher url="/api/products">
{({ data, isLoading }) =>
isLoading ? <Skeleton /> : <ProductGrid items={data} />
}
</DataFetcher>function Toggle({ children, initialOn = false }) {
const [on, setOn] = useState(initialOn)
const toggle = () => setOn(prev => !prev)
const turnOn = () => setOn(true)
const turnOff = () => setOn(false)
// Передаём API управления состоянием
return children({ on, toggle, turnOn, turnOff })
}
// Использование 1: простая кнопка
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>{on ? 'ВКЛ' : 'ВЫКЛ'}</button>
)}
</Toggle>
// Использование 2: сложный UI с тем же состоянием
<Toggle initialOn={true}>
{({ on, toggle, turnOff }) => (
<div>
<span>Тёмная тема: {on ? 'да' : 'нет'}</span>
<button onClick={toggle}>Переключить</button>
<button onClick={turnOff}>Сбросить</button>
</div>
)}
</Toggle>// Один сценарий — три реализации:
// 1. HOC:
const ToggleButton = withToggle(Button)
// Минус: пропсы приходят магически, непрозрачно
// 2. Render Props:
<Toggle>{({ on, toggle }) => <Button on={on} onClick={toggle} />}</Toggle>
// Плюс: явно видно что передаётся; Минус: вложенность
// 3. Кастомный хук (предпочтительно сейчас):
function ToggleButton() {
const { on, toggle } = useToggle()
return <Button on={on} onClick={toggle} />
}1. Библиотеки форм — Formik использует render props: <Field>{({ field }) => <input {...field} />}</Field>
2. Передача нескольких значений — удобно читается без деструктуризации
3. Динамическое управление рендером — когда нужно полностью контролировать что рисуется
4. Несколько "слотов" — компонент с несколькими функциями-пропсами
<DataGrid
data={items}
renderHeader={() => <thead>...</thead>}
renderRow={(item) => <tr key={item.id}>{item.name}</tr>}
renderEmpty={() => <p>Нет данных</p>}
/>Реализация паттерна Render Props через функции JavaScript: DataProvider с children-функцией, Toggle компонент, и DataFetcher с состояниями загрузки
// Реализуем паттерн Render Props через обычные функции.
// В React children — функция. Здесь — любой callback.
// --- Компонент 1: Toggle (управляет булевым состоянием) ---
function createToggle(initialOn = false) {
let on = initialOn
const listeners = []
function notify() {
listeners.forEach(fn => fn({ on, toggle, turnOn, turnOff }))
}
function toggle() {
on = !on
notify()
}
function turnOn() { on = true; notify() }
function turnOff() { on = false; notify() }
// "render" = вызов children-функции с текущим состоянием
function render(childrenFn) {
return childrenFn({ on, toggle, turnOn, turnOff })
}
// subscribe = аналог useEffect для реактивности
function subscribe(fn) { listeners.push(fn) }
return { render, toggle, turnOn, turnOff, subscribe }
}
console.log('=== Toggle с render prop ===')
const toggle1 = createToggle(false)
// "Рендерим" разные UI с одним состоянием
const ui1 = toggle1.render(({ on, toggle }) =>
'Кнопка: [' + (on ? '● ВКЛ' : '○ ВЫКЛ') + '] (click=toggle)'
)
console.log(ui1) // [○ ВЫКЛ]
toggle1.toggle()
const ui2 = toggle1.render(({ on, turnOff }) =>
'Чекбокс: ' + (on ? '☑' : '☐') + ' Тёмная тема | Кнопка [Сбросить]'
)
console.log(ui2) // ☑ Тёмная тема
// --- Компонент 2: DataFetcher (управляет загрузкой данных) ---
function createDataFetcher(fetchFn) {
let state = { data: null, isLoading: false, error: null }
async function load(params) {
state = { data: null, isLoading: true, error: null }
try {
const data = await fetchFn(params)
state = { data, isLoading: false, error: null }
} catch (err) {
state = { data: null, isLoading: false, error: err.message }
}
return state
}
// render prop — вызывается с текущим состоянием
function render(childrenFn) {
return childrenFn(state)
}
return { load, render }
}
// Использование 1: список пользователей
async function testDataFetcher() {
const fakeApi = (endpoint) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (endpoint === '/users') {
resolve([
{ id: 1, name: 'Алексей' },
{ id: 2, name: 'Мария' },
])
} else {
reject(new Error('404 Not Found'))
}
}, 50)
})
}
const fetcher1 = createDataFetcher(fakeApi)
console.log('
=== DataFetcher: состояния загрузки ===')
// До загрузки
await fetcher1.load('/users')
const result = fetcher1.render(({ data, isLoading, error }) => {
if (isLoading) return '[Загрузка...]'
if (error) return '[Ошибка: ' + error + ']'
return '[Список: ' + data.map(u => u.name).join(', ') + ']'
})
console.log('Результат:', result) // [Список: Алексей, Мария]
// Ошибка
const fetcher2 = createDataFetcher(fakeApi)
await fetcher2.load('/nonexistent')
const errorResult = fetcher2.render(({ error }) =>
error ? '[Ошибка: ' + error + ']' : '[Данные]'
)
console.log('Ошибка:', errorResult)
}
// --- Компонент 3: SortableList (render props для строк) ---
function createSortableList(items) {
let sortedItems = [...items]
let sortKey = null
let sortDir = 'asc'
function sortBy(key) {
if (sortKey === key) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc'
} else {
sortKey = key
sortDir = 'asc'
}
sortedItems = [...items].sort((a, b) => {
const val = sortDir === 'asc'
? String(a[key]).localeCompare(String(b[key]))
: String(b[key]).localeCompare(String(a[key]))
return val
})
}
// render prop: renderRow вызывается для каждого элемента
function render(renderRow) {
return sortedItems.map(renderRow)
}
return { sortBy, render }
}
console.log('
=== SortableList с render prop ===')
const list = createSortableList([
{ name: 'Мария', city: 'Москва' },
{ name: 'Алексей', city: 'СПб' },
{ name: 'Ирина', city: 'Казань' },
])
console.log('Исходный порядок:')
list.render(item => console.log(' -', item.name, '|', item.city))
list.sortBy('name')
console.log('
Сортировка по имени:')
list.render(item => console.log(' -', item.name, '|', item.city))
testDataFetcher()Изучи материал выше и задай вопросы AI если что-то непонятно
Этот урок содержит теоретическую информацию. Изучи материал, затем отметь урок как пройденный.