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 решают задачу повторного использования логики между компонентами. Классические примеры:
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 можно комбинировать:
// Ручная композиция:
const EnhancedComponent = withLogger(withAuth(withErrorBoundary(UserProfile)))
// Через утилиту compose (как в Redux):
const enhance = compose(withLogger, withAuth, withErrorBoundary)
const EnhancedProfile = enhance(UserProfile)
// Порядок важен: сначала применяется самый правый
// withErrorBoundary -> withAuth -> withLogger -> UserProfileДо React Hooks (до 2019 года) HOC были основным способом переиспользовать логику. Теперь в большинстве случаев предпочтительны кастомные хуки:
| | HOC | Кастомный хук |
|---|---|---|
| Синтаксис | Компонент-обёртка | Функция |
| Вложенность | Глубокая (wrapper hell) | Плоская |
| Пропсы | Добавляет новые пропсы | Возвращает значения |
| Условное применение | Нельзя | Можно (внутри компонента) |
| DevTools | Обёртки видны | Чище |
| Когда применять | Изменение JSX-вывода | Разделение логики |
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() {
// Хуки не могут менять что рендерится в компоненте-родителе
}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' })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 решают задачу повторного использования логики между компонентами. Классические примеры:
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 можно комбинировать:
// Ручная композиция:
const EnhancedComponent = withLogger(withAuth(withErrorBoundary(UserProfile)))
// Через утилиту compose (как в Redux):
const enhance = compose(withLogger, withAuth, withErrorBoundary)
const EnhancedProfile = enhance(UserProfile)
// Порядок важен: сначала применяется самый правый
// withErrorBoundary -> withAuth -> withLogger -> UserProfileДо React Hooks (до 2019 года) HOC были основным способом переиспользовать логику. Теперь в большинстве случаев предпочтительны кастомные хуки:
| | HOC | Кастомный хук |
|---|---|---|
| Синтаксис | Компонент-обёртка | Функция |
| Вложенность | Глубокая (wrapper hell) | Плоская |
| Пропсы | Добавляет новые пропсы | Возвращает значения |
| Условное применение | Нельзя | Можно (внутри компонента) |
| DevTools | Обёртки видны | Чище |
| Когда применять | Изменение JSX-вывода | Разделение логики |
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() {
// Хуки не могут менять что рендерится в компоненте-родителе
}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} />