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

HOC: компоненты высшего порядка

Что такое HOC

Higher Order Component (HOC) — это функция, которая принимает компонент и возвращает новый, улучшенный компонент. Название пришло из функционального программирования, где Higher Order Function принимает функцию и возвращает новую функцию.

// Обычная функция высшего порядка:
const double = fn => x => fn(fn(x))
const addOne = x => x + 1
const addTwo = double(addOne)  // addTwo(3) === 5

// HOC — то же самое для компонентов:
const withLogger = Component => {
  return function WrappedComponent(props) {
    console.log('Рендер:', Component.name, 'с пропсами:', props)
    return <Component {...props} />
  }
}

const LoggedButton = withLogger(Button)
// LoggedButton рендерит Button, но логирует каждый рендер

Зачем нужны HOC

HOC решают задачу повторного использования логики между компонентами. Классические примеры:

withAuth — защита маршрутов:

function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { isAuthenticated, user } = useAuth()

    if (!isAuthenticated) {
      return <Redirect to="/login" />
    }

    return <Component {...props} currentUser={user} />
  }
}

// Использование:
const ProtectedDashboard = withAuth(Dashboard)
const ProtectedProfile = withAuth(Profile)

withLoading — индикатор загрузки:

function withLoading(Component) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <Spinner />
    }
    return <Component {...props} />
  }
}

const UserListWithLoading = withLoading(UserList)
// <UserListWithLoading isLoading={isFetching} users={users} />

Соглашения об именовании

1. Называйте HOC с префикса with: withAuth, withLogger, withErrorBoundary

2. Называйте обёртку Wrapped[ComponentName] или With[Feature][ComponentName]

3. Задавайте displayName для удобства в DevTools:

function withLogger(Component) {
  function WrappedComponent(props) {
    // ...
  }
  // Имя в React DevTools:
  WrappedComponent.displayName = 'withLogger(' + (Component.displayName || Component.name) + ')'
  return WrappedComponent
}

Композиция HOC

HOC можно комбинировать:

// Ручная композиция:
const EnhancedComponent = withLogger(withAuth(withErrorBoundary(UserProfile)))

// Через утилиту compose (как в Redux):
const enhance = compose(withLogger, withAuth, withErrorBoundary)
const EnhancedProfile = enhance(UserProfile)

// Порядок важен: сначала применяется самый правый
// withErrorBoundary -> withAuth -> withLogger -> UserProfile

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

До React Hooks (до 2019 года) HOC были основным способом переиспользовать логику. Теперь в большинстве случаев предпочтительны кастомные хуки:

| | HOC | Кастомный хук |

|---|---|---|

| Синтаксис | Компонент-обёртка | Функция |

| Вложенность | Глубокая (wrapper hell) | Плоская |

| Пропсы | Добавляет новые пропсы | Возвращает значения |

| Условное применение | Нельзя | Можно (внутри компонента) |

| DevTools | Обёртки видны | Чище |

| Когда применять | Изменение JSX-вывода | Разделение логики |

Когда HOC всё ещё нужны

1. Изменение рендера — добавить обёртку, условный рендер (withAuth возвращает Redirect)

2. Совместимость — HOC можно применять к классовым компонентам, хуки — нет

3. Библиотеки — connect() из Redux, withRouter из React Router (старый API)

4. Перехват и изменение пропсов перед передачей в компонент

// HOC нужен: мы хотим другой JSX при ошибке
function withErrorBoundary(Component, FallbackUI) {
  return class extends React.Component {
    state = { hasError: false }
    static getDerivedStateFromError() { return { hasError: true } }
    render() {
      if (this.state.hasError) return <FallbackUI />  // другой JSX
      return <Component {...this.props} />
    }
  }
}

// Хук НЕ может вернуть другой JSX — только значения:
function useErrorBoundary() {
  // Хуки не могут менять что рендерится в компоненте-родителе
}

Проблемы HOC

1. Props collision — HOC может затереть пропс с тем же именем

2. Wrapper hell — много HOC = глубокое дерево компонентов

3. Ref не передаётся — нужен React.forwardRef

4. Непрозрачность — откуда пришёл этот проп?

Примеры

Реализация паттерна HOC через обычные функции JavaScript: withLogger, withAuth, withErrorHandler и функция compose для комбинирования

// Демонстрируем паттерн HOC на чистом JavaScript.
// Компонент = функция, HOC = функция высшего порядка.

// --- Базовые "компоненты" (функции) ---

function UserCard(props) {
  return 'UserCard { name: ' + props.name + ', role: ' + props.role + ' }'
}

function ProductList(props) {
  if (!props.items || props.items.length === 0) {
    return 'ProductList { пустой список }'
  }
  return 'ProductList { ' + props.items.length + ' товаров: ' + props.items.join(', ') + ' }'
}

// --- HOC 1: withLogger ---
// Логирует каждый "рендер" (вызов) компонента

function withLogger(Component) {
  function WrappedComponent(props) {
    console.log('[withLogger] Рендер: ' + (Component.displayName || Component.name))
    console.log('[withLogger] Пропсы:', JSON.stringify(props))
    const result = Component(props)
    console.log('[withLogger] Результат:', result)
    return result
  }
  WrappedComponent.displayName = 'withLogger(' + Component.name + ')'
  return WrappedComponent
}

// --- HOC 2: withAuth ---
// Проверяет авторизацию, иначе возвращает заглушку

function withAuth(Component) {
  function AuthenticatedComponent(props) {
    const { isAuthenticated, ...rest } = props

    if (!isAuthenticated) {
      return '[РЕДИРЕКТ] Требуется авторизация → /login'
    }

    return Component(rest)
  }
  AuthenticatedComponent.displayName = 'withAuth(' + Component.name + ')'
  return AuthenticatedComponent
}

// --- HOC 3: withLoadingState ---
// Показывает загрузку или ошибку вместо компонента

function withLoadingState(Component) {
  function WithLoadingComponent(props) {
    const { isLoading, error, ...rest } = props

    if (isLoading) return '[Загрузка...]'
    if (error)     return '[Ошибка: ' + error + ']'
    return Component(rest)
  }
  WithLoadingComponent.displayName = 'withLoadingState(' + Component.name + ')'
  return WithLoadingComponent
}

// --- HOC 4: withDefaultProps ---
// Добавляет дефолтные значения к пропсам

function withDefaultProps(Component, defaults) {
  function WithDefaultsComponent(props) {
    const mergedProps = { ...defaults, ...props }
    return Component(mergedProps)
  }
  WithDefaultsComponent.displayName = 'withDefaultProps(' + Component.name + ')'
  return WithDefaultsComponent
}

// --- Утилита compose ---
// Применяет HOC справа налево

function compose(...hocs) {
  return (Component) => hocs.reduceRight((acc, hoc) => hoc(acc), Component)
}

// --- Тесты ---

console.log('=== HOC withLogger ===')
const LoggedUserCard = withLogger(UserCard)
LoggedUserCard({ name: 'Алексей', role: 'Разработчик' })

console.log('
=== HOC withAuth ===')
const AuthUserCard = withAuth(UserCard)
console.log('Неавторизован:', AuthUserCard({ isAuthenticated: false, name: 'Алексей' }))
console.log('Авторизован:', AuthUserCard({ isAuthenticated: true, name: 'Алексей', role: 'Admin' }))

console.log('
=== HOC withLoadingState ===')
const SafeProductList = withLoadingState(ProductList)
console.log('Загрузка:', SafeProductList({ isLoading: true }))
console.log('Ошибка:', SafeProductList({ isLoading: false, error: 'Нет соединения' }))
console.log('Данные:', SafeProductList({ isLoading: false, items: ['iPhone', 'MacBook'] }))

console.log('
=== HOC withDefaultProps ===')
const CardWithDefaults = withDefaultProps(UserCard, { role: 'Пользователь', name: 'Гость' })
console.log('Только name:', CardWithDefaults({ name: 'Мария' }))  // role = 'Пользователь'
console.log('Все пропсы:', CardWithDefaults({ name: 'Алексей', role: 'Admin' }))

console.log('
=== Композиция HOC ===')
// Применяем несколько HOC сразу
const enhance = compose(withLogger, withAuth, withLoadingState)
const EnhancedUserCard = enhance(UserCard)

console.log('displayName:', EnhancedUserCard.displayName)
// withLogger(withAuth(withLoadingState(UserCard)))

console.log('
Неавторизован + загрузка:')
EnhancedUserCard({ isAuthenticated: false, isLoading: true, name: 'Алексей' })

console.log('
Авторизован с данными:')
EnhancedUserCard({ isAuthenticated: true, isLoading: false, name: 'Мария', role: 'Manager' })

HOC: компоненты высшего порядка

Что такое HOC

Higher Order Component (HOC) — это функция, которая принимает компонент и возвращает новый, улучшенный компонент. Название пришло из функционального программирования, где Higher Order Function принимает функцию и возвращает новую функцию.

// Обычная функция высшего порядка:
const double = fn => x => fn(fn(x))
const addOne = x => x + 1
const addTwo = double(addOne)  // addTwo(3) === 5

// HOC — то же самое для компонентов:
const withLogger = Component => {
  return function WrappedComponent(props) {
    console.log('Рендер:', Component.name, 'с пропсами:', props)
    return <Component {...props} />
  }
}

const LoggedButton = withLogger(Button)
// LoggedButton рендерит Button, но логирует каждый рендер

Зачем нужны HOC

HOC решают задачу повторного использования логики между компонентами. Классические примеры:

withAuth — защита маршрутов:

function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { isAuthenticated, user } = useAuth()

    if (!isAuthenticated) {
      return <Redirect to="/login" />
    }

    return <Component {...props} currentUser={user} />
  }
}

// Использование:
const ProtectedDashboard = withAuth(Dashboard)
const ProtectedProfile = withAuth(Profile)

withLoading — индикатор загрузки:

function withLoading(Component) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <Spinner />
    }
    return <Component {...props} />
  }
}

const UserListWithLoading = withLoading(UserList)
// <UserListWithLoading isLoading={isFetching} users={users} />

Соглашения об именовании

1. Называйте HOC с префикса with: withAuth, withLogger, withErrorBoundary

2. Называйте обёртку Wrapped[ComponentName] или With[Feature][ComponentName]

3. Задавайте displayName для удобства в DevTools:

function withLogger(Component) {
  function WrappedComponent(props) {
    // ...
  }
  // Имя в React DevTools:
  WrappedComponent.displayName = 'withLogger(' + (Component.displayName || Component.name) + ')'
  return WrappedComponent
}

Композиция HOC

HOC можно комбинировать:

// Ручная композиция:
const EnhancedComponent = withLogger(withAuth(withErrorBoundary(UserProfile)))

// Через утилиту compose (как в Redux):
const enhance = compose(withLogger, withAuth, withErrorBoundary)
const EnhancedProfile = enhance(UserProfile)

// Порядок важен: сначала применяется самый правый
// withErrorBoundary -> withAuth -> withLogger -> UserProfile

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

До React Hooks (до 2019 года) HOC были основным способом переиспользовать логику. Теперь в большинстве случаев предпочтительны кастомные хуки:

| | HOC | Кастомный хук |

|---|---|---|

| Синтаксис | Компонент-обёртка | Функция |

| Вложенность | Глубокая (wrapper hell) | Плоская |

| Пропсы | Добавляет новые пропсы | Возвращает значения |

| Условное применение | Нельзя | Можно (внутри компонента) |

| DevTools | Обёртки видны | Чище |

| Когда применять | Изменение JSX-вывода | Разделение логики |

Когда HOC всё ещё нужны

1. Изменение рендера — добавить обёртку, условный рендер (withAuth возвращает Redirect)

2. Совместимость — HOC можно применять к классовым компонентам, хуки — нет

3. Библиотеки — connect() из Redux, withRouter из React Router (старый API)

4. Перехват и изменение пропсов перед передачей в компонент

// HOC нужен: мы хотим другой JSX при ошибке
function withErrorBoundary(Component, FallbackUI) {
  return class extends React.Component {
    state = { hasError: false }
    static getDerivedStateFromError() { return { hasError: true } }
    render() {
      if (this.state.hasError) return <FallbackUI />  // другой JSX
      return <Component {...this.props} />
    }
  }
}

// Хук НЕ может вернуть другой JSX — только значения:
function useErrorBoundary() {
  // Хуки не могут менять что рендерится в компоненте-родителе
}

Проблемы HOC

1. Props collision — HOC может затереть пропс с тем же именем

2. Wrapper hell — много HOC = глубокое дерево компонентов

3. Ref не передаётся — нужен React.forwardRef

4. Непрозрачность — откуда пришёл этот проп?

Примеры

Реализация паттерна HOC через обычные функции JavaScript: withLogger, withAuth, withErrorHandler и функция compose для комбинирования

// Демонстрируем паттерн HOC на чистом JavaScript.
// Компонент = функция, HOC = функция высшего порядка.

// --- Базовые "компоненты" (функции) ---

function UserCard(props) {
  return 'UserCard { name: ' + props.name + ', role: ' + props.role + ' }'
}

function ProductList(props) {
  if (!props.items || props.items.length === 0) {
    return 'ProductList { пустой список }'
  }
  return 'ProductList { ' + props.items.length + ' товаров: ' + props.items.join(', ') + ' }'
}

// --- HOC 1: withLogger ---
// Логирует каждый "рендер" (вызов) компонента

function withLogger(Component) {
  function WrappedComponent(props) {
    console.log('[withLogger] Рендер: ' + (Component.displayName || Component.name))
    console.log('[withLogger] Пропсы:', JSON.stringify(props))
    const result = Component(props)
    console.log('[withLogger] Результат:', result)
    return result
  }
  WrappedComponent.displayName = 'withLogger(' + Component.name + ')'
  return WrappedComponent
}

// --- HOC 2: withAuth ---
// Проверяет авторизацию, иначе возвращает заглушку

function withAuth(Component) {
  function AuthenticatedComponent(props) {
    const { isAuthenticated, ...rest } = props

    if (!isAuthenticated) {
      return '[РЕДИРЕКТ] Требуется авторизация → /login'
    }

    return Component(rest)
  }
  AuthenticatedComponent.displayName = 'withAuth(' + Component.name + ')'
  return AuthenticatedComponent
}

// --- HOC 3: withLoadingState ---
// Показывает загрузку или ошибку вместо компонента

function withLoadingState(Component) {
  function WithLoadingComponent(props) {
    const { isLoading, error, ...rest } = props

    if (isLoading) return '[Загрузка...]'
    if (error)     return '[Ошибка: ' + error + ']'
    return Component(rest)
  }
  WithLoadingComponent.displayName = 'withLoadingState(' + Component.name + ')'
  return WithLoadingComponent
}

// --- HOC 4: withDefaultProps ---
// Добавляет дефолтные значения к пропсам

function withDefaultProps(Component, defaults) {
  function WithDefaultsComponent(props) {
    const mergedProps = { ...defaults, ...props }
    return Component(mergedProps)
  }
  WithDefaultsComponent.displayName = 'withDefaultProps(' + Component.name + ')'
  return WithDefaultsComponent
}

// --- Утилита compose ---
// Применяет HOC справа налево

function compose(...hocs) {
  return (Component) => hocs.reduceRight((acc, hoc) => hoc(acc), Component)
}

// --- Тесты ---

console.log('=== HOC withLogger ===')
const LoggedUserCard = withLogger(UserCard)
LoggedUserCard({ name: 'Алексей', role: 'Разработчик' })

console.log('
=== HOC withAuth ===')
const AuthUserCard = withAuth(UserCard)
console.log('Неавторизован:', AuthUserCard({ isAuthenticated: false, name: 'Алексей' }))
console.log('Авторизован:', AuthUserCard({ isAuthenticated: true, name: 'Алексей', role: 'Admin' }))

console.log('
=== HOC withLoadingState ===')
const SafeProductList = withLoadingState(ProductList)
console.log('Загрузка:', SafeProductList({ isLoading: true }))
console.log('Ошибка:', SafeProductList({ isLoading: false, error: 'Нет соединения' }))
console.log('Данные:', SafeProductList({ isLoading: false, items: ['iPhone', 'MacBook'] }))

console.log('
=== HOC withDefaultProps ===')
const CardWithDefaults = withDefaultProps(UserCard, { role: 'Пользователь', name: 'Гость' })
console.log('Только name:', CardWithDefaults({ name: 'Мария' }))  // role = 'Пользователь'
console.log('Все пропсы:', CardWithDefaults({ name: 'Алексей', role: 'Admin' }))

console.log('
=== Композиция HOC ===')
// Применяем несколько HOC сразу
const enhance = compose(withLogger, withAuth, withLoadingState)
const EnhancedUserCard = enhance(UserCard)

console.log('displayName:', EnhancedUserCard.displayName)
// withLogger(withAuth(withLoadingState(UserCard)))

console.log('
Неавторизован + загрузка:')
EnhancedUserCard({ isAuthenticated: false, isLoading: true, name: 'Алексей' })

console.log('
Авторизован с данными:')
EnhancedUserCard({ isAuthenticated: true, isLoading: false, name: 'Мария', role: 'Manager' })

Задание

Реализуй HOC `withLoadingState(Component)`, который оборачивает React-компонент и добавляет обработку состояний загрузки. HOC должен: принимать пропсы `isLoading` и `error`; при `isLoading === true` показывать спиннер; при наличии `error` показывать сообщение об ошибке; иначе рендерить оригинальный компонент с остальными пропсами.

Подсказка

Для проверки isLoading: if (isLoading) { return <div>...</div> }. Для проверки error: if (error) { return <div>Ошибка: {error}</div> }. Для рендера компонента: return <Component {...rest} />

Загружаем среду выполнения...
Загружаем AI-помощника...