Что такое компоненты высшего порядка (Higher-Order Components, HOC)?

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Средний
#React

Краткий ответ

Компонент высшего порядка (HOC) — это функция, которая принимает компонент и возвращает новый компонент с расширенной функциональностью. Это паттерн композиции для переиспользования логики компонентов.

Базовый пример HOC:

// HOC для добавления логирования
const withLogging = (WrappedComponent) => {
  return function WithLoggingComponent(props) {
    useEffect(() => {
      console.log('Component mounted:', WrappedComponent.name);
    }, []);
    
    return <WrappedComponent {...props} />;
  };
};
 
// Использование
const Button = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);
 
const ButtonWithLogging = withLogging(Button);

Полный ответ

HOC — это продвинутый паттерн в React для переиспользования логики компонентов. Они не являются частью React API, а возникают из композиционной природы React.

Анатомия HOC

Базовая структура

// HOC принимает компонент
const withEnhancement = (WrappedComponent) => {
  // Возвращает новый компонент
  const EnhancedComponent = (props) => {
    // Добавляет новую логику
    const [state, setState] = useState();
    
    // Рендерит оригинальный компонент с новыми пропсами
    return (
      <WrappedComponent 
        {...props} 
        enhancedProp={state}
      />
    );
  };
  
  // Задаём displayName для отладки
  EnhancedComponent.displayName = 
    `withEnhancement(${WrappedComponent.displayName || WrappedComponent.name})`;
  
  return EnhancedComponent;
};

Распространённые примеры HOC

1. HOC для аутентификации

const withAuth = (WrappedComponent) => {
  return function WithAuthComponent(props) {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [isLoading, setIsLoading] = useState(true);
    
    useEffect(() => {
      // Проверка аутентификации
      checkAuth()
        .then(authenticated => {
          setIsAuthenticated(authenticated);
          setIsLoading(false);
        });
    }, []);
    
    if (isLoading) {
      return <div>Загрузка...</div>;
    }
    
    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }
    
    return <WrappedComponent {...props} />;
  };
};
 
// Использование
const Dashboard = () => <div>Панель управления</div>;
const ProtectedDashboard = withAuth(Dashboard);

2. HOC для загрузки данных

const withDataFetching = (url) => (WrappedComponent) => {
  return function WithDataFetchingComponent(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
      fetch(url)
        .then(res => res.json())
        .then(data => {
          setData(data);
          setLoading(false);
        })
        .catch(err => {
          setError(err);
          setLoading(false);
        });
    }, []);
    
    return (
      <WrappedComponent 
        {...props}
        data={data}
        loading={loading}
        error={error}
      />
    );
  };
};
 
// Использование
const UserList = ({ data, loading, error }) => {
  if (loading) return <div>Загрузка...</div>;
  if (error) return <div>Ошибка: {error.message}</div>;
  
  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};
 
const UsersWithData = withDataFetching('/api/users')(UserList);

3. HOC для отслеживания размеров

const withWindowSize = (WrappedComponent) => {
  return function WithWindowSizeComponent(props) {
    const [windowSize, setWindowSize] = useState({
      width: window.innerWidth,
      height: window.innerHeight
    });
    
    useEffect(() => {
      const handleResize = () => {
        setWindowSize({
          width: window.innerWidth,
          height: window.innerHeight
        });
      };
      
      window.addEventListener('resize', handleResize);
      
      return () => {
        window.removeEventListener('resize', handleResize);
      };
    }, []);
    
    return (
      <WrappedComponent 
        {...props}
        windowSize={windowSize}
      />
    );
  };
};
 
// Использование
const ResponsiveComponent = ({ windowSize }) => (
  <div>
    Ширина: {windowSize.width}px
    {windowSize.width < 768 ? ' (Мобильный)' : ' (Десктоп)'}
  </div>
);
 
const ResponsiveWithSize = withWindowSize(ResponsiveComponent);

Композиция HOC

Последовательное применение

// Несколько HOC подряд
const enhance = compose(
  withAuth,
  withWindowSize,
  withLogging
);
 
const EnhancedComponent = enhance(BaseComponent);
 
// Или вручную
const EnhancedComponent = 
  withAuth(
    withWindowSize(
      withLogging(BaseComponent)
    )
  );

Реализация compose

const compose = (...hocs) => (Component) =>
  hocs.reduceRight((acc, hoc) => hoc(acc), Component);
 
// Использование
const enhance = compose(
  withRouter,
  withAuth,
  connect(mapStateToProps)
);
 
const EnhancedApp = enhance(App);

Продвинутые паттерны

1. HOC с конфигурацией

const withTheme = (theme = 'light') => (WrappedComponent) => {
  return function WithThemeComponent(props) {
    const themeConfig = {
      light: {
        background: '#fff',
        color: '#000'
      },
      dark: {
        background: '#000',
        color: '#fff'
      }
    };
    
    return (
      <div style={themeConfig[theme]}>
        <WrappedComponent {...props} theme={theme} />
      </div>
    );
  };
};
 
// Использование
const ThemedButton = withTheme('dark')(Button);

2. HOC с ref forwarding

const withRefForwarding = (WrappedComponent) => {
  const WithRef = React.forwardRef((props, ref) => {
    return <WrappedComponent {...props} forwardedRef={ref} />;
  });
  
  WithRef.displayName = 
    `withRefForwarding(${WrappedComponent.displayName || WrappedComponent.name})`;
  
  return WithRef;
};
 
// Компонент, использующий ref
const FancyInput = ({ forwardedRef, ...props }) => (
  <input ref={forwardedRef} {...props} />
);
 
const ForwardedInput = withRefForwarding(FancyInput);
 
// Использование
const App = () => {
  const inputRef = useRef();
  
  return <ForwardedInput ref={inputRef} />;
};

Лучшие практики

1. Не изменяйте оригинальный компонент

// ❌ Плохо: мутация компонента
const withBadEnhancement = (WrappedComponent) => {
  WrappedComponent.prototype.componentDidUpdate = function() {
    console.log('Updated!');
  };
  return WrappedComponent;
};
 
// ✅ Хорошо: композиция
const withGoodEnhancement = (WrappedComponent) => {
  return class extends React.Component {
    componentDidUpdate() {
      console.log('Updated!');
    }
    
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

2. Передавайте все пропсы

// ❌ Плохо: теряем пропсы
const withBadHOC = (WrappedComponent) => {
  return function BadHOC({ someProp }) {
    return <WrappedComponent someProp={someProp} />;
  };
};
 
// ✅ Хорошо: передаём все пропсы
const withGoodHOC = (WrappedComponent) => {
  return function GoodHOC(props) {
    return <WrappedComponent {...props} />;
  };
};

3. Максимизируйте композицию

// ❌ Плохо: HOC делает слишком много
const withEverything = (WrappedComponent) => {
  return function WithEverything(props) {
    // Аутентификация
    // Загрузка данных
    // Тема
    // Логирование
    // ...много логики
  };
};
 
// ✅ Хорошо: отдельные HOC для каждой задачи
const enhance = compose(
  withAuth,
  withDataFetching('/api/data'),
  withTheme('dark'),
  withLogging
);

HOC vs другие паттерны

HOC vs Render Props

// HOC
const withMouse = (Component) => (props) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ... логика
  return <Component {...props} mouse={position} />;
};
 
// Render Props
const Mouse = ({ render }) => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ... логика
  return render(position);
};
 
// Использование Render Props
<Mouse render={position => <Component mouse={position} />} />

HOC vs Custom Hooks

// HOC
const withWindowSize = (Component) => (props) => {
  const [size, setSize] = useState(getWindowSize());
  // ... логика
  return <Component {...props} windowSize={size} />;
};
 
// Custom Hook
const useWindowSize = () => {
  const [size, setSize] = useState(getWindowSize());
  // ... логика
  return size;
};
 
// Использование Hook
const Component = () => {
  const windowSize = useWindowSize();
  // ... использование windowSize
};

Когда использовать HOC

Подходит для:

  • Переиспользования логики между многими компонентами
  • Добавления функциональности без изменения компонента
  • Условного рендеринга на основе внешних факторов
  • Инжекции пропсов из внешних источников

Не подходит когда:

  • Нужна более гибкая композиция (используйте hooks)
  • Логика специфична для одного компонента
  • Требуется доступ к внутреннему состоянию компонента

Отладка HOC

// Установка displayName для DevTools
const withDebugInfo = (WrappedComponent) => {
  const WithDebugInfo = (props) => {
    useEffect(() => {
      console.log(`${WrappedComponent.name} rendered with props:`, props);
    });
    
    return <WrappedComponent {...props} />;
  };
  
  // Важно для React DevTools
  WithDebugInfo.displayName = 
    `withDebugInfo(${WrappedComponent.displayName || WrappedComponent.name})`;
  
  return WithDebugInfo;
};

HOC остаются мощным инструментом для создания переиспользуемой логики, хотя в современном React часто предпочитают использовать хуки для решения тех же задач.


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪