← React/Render Props: функции как дети#281 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksМаршрут: старт с нуля

Render Props: функции как дети

Что такое Render Props

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} />} />

Суть паттерна: компонент управляет состоянием, вы управляете отрисовкой.

Классический пример: MouseTracker

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>

DataFetcher с Render Props

Самый практичный пример — компонент для загрузки данных:

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>

Toggle — классический пример с состоянием

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>

Render Props vs HOC vs Кастомные хуки

// Один сценарий — три реализации:

// 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} />
}

Когда Render Props всё ещё уместны

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: функции как дети

Что такое Render Props

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} />} />

Суть паттерна: компонент управляет состоянием, вы управляете отрисовкой.

Классический пример: MouseTracker

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>

DataFetcher с Render Props

Самый практичный пример — компонент для загрузки данных:

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>

Toggle — классический пример с состоянием

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>

Render Props vs HOC vs Кастомные хуки

// Один сценарий — три реализации:

// 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} />
}

Когда Render Props всё ещё уместны

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 если что-то непонятно

Этот урок содержит теоретическую информацию. Изучи материал, затем отметь урок как пройденный.

Загружаем AI-помощника...