Почему не стоит использовать компоненты высшего порядка в Реакте

Вокруг Реакта сложилось достаточно много паттернов, один из них — компоненты высшего порядка. Они призваны сделать удобным реиспользование общей функциональности между компонентами: эта функциональность выносится в компонент высшего порядка, а компоненты, требующие её, перед использованием просто оборачиваются в HOC. В теории звучит здорово, но на практике у компонентов высшего порядка есть большой недостаток: они похожи на наследование.

Концептуальная проблема

В чём проблема наследования? В том, что каждый уровень в иерархии классов влияет на следующие уровни. Метод любого класса будет распространён на все дочерние классы, которые, в свою очередь, объявляют собственные методы и могут переопределять унаследованные. Чтобы понять, что доступно в конкретном классе, нужно держать в голове всю цепочку родительских классов с их собственными и переопределенными методами.

Это и есть общая проблема наследования и компонентов высшего порядка: на конкретную программную сущность (класс или компонент) влияют внешние сущности (родительский класс или компонент высшего порядка), и чтобы ей корректно пользоваться, разработчику приходится знать и помнить результаты этого влияния.

У каждого компонента в Реакте есть свой API, называемый пропами. Пропы — мост между компонентом и внешним миром. Любой компонент высшего порядка — дополнительный слой между обёрнутым компонентом и внешним миром, который влияет на обе стороны: он может передавать какие-то пропы обёрнутому компоненту или ожидать какие-то пропы от внешнего мира. В итоге разработчик вынужден помнить об этом неявном изменении контракта между обёрнутым компонентом и внешним миром.

Альтернатива: рендер-пропы

Компоненты высшего порядка — не единственный паттерн для реиспользования общей функциональности. Общую функциональность можно вынести в отдельный компонент и сделать её доступной через рендер-пропы: при таком подходе любые данные передаются явно и ничего не нарушает контракт компонентов с внешним миром. Пример (код максимально упрощён и далёк от реальных кейсов в угоду наглядности):

/**
 * Подход с компонентами высшего порядка
 */
import { message } from 'antd';

const Button = (props) => <button {...props} />;

const withMessage = (WrappedComponent) => (
  ({ messageType, messageText, ...restProps }) => (
    <WrappedComponent {...restProps} onClick={() => message[messageType](messageText)} />
  )
);

const ButtonWithMessage = withMessage(Button);

<ButtonWithMessage messageType="success" messageText="Hello!">Say hello</ButtonWithMessage>
/**
 * Подход с рендер-пропами
 */
import { message } from 'antd';

const Button = (props) => <button {...props} />;

const MessageProvider = ({ type, text, children }) => children({ showMessage: () => message[type](text) });

<MessageProvider type="success" text="hello">
  {({ showMessage}) => (
    <Button onClick={showMessage}>Say hello</Button>
  )}
</MessageProvider>