Компонент более высокого порядка (Higher-Order-Component - HOC) является передовым методом в React для повторного использования компонентной логики. HOC не являются частью React API, как таковой. Они представляют собой образец, который возникает из композиционной природы Реакта.

Конкретно, компонент более высокого порядка - это функция, которая принимает компонент и возвращает новый компонент.

const EnhancedComponent = higherOrderComponent(WrappedComponent);

 В то время как компонент преобразует реквизит в пользовательский интерфейс, компонент более высокого порядка преобразует компонент в другой компонент. HOC распространены в сторонних библиотеках React, таких как Redux connect и Relay createFragmentContainer

Использовать HOC для перекрестных проблем

Компоненты являются основной единицей повторного использования кода в React. Однако вы обнаружите, что некоторые шаблоны не подходят для традиционных компонентов. Например, скажем, у вас есть CommentList компонент, который подписывается на внешний источник данных для отображения списка комментариев:

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

Позже вы пишете компонент для подписки на одно сообщение в блоге, которое следует аналогичной схеме:

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

CommentList и BlogPost они не идентичны - они называет разные методы DataSource, и они выводят разные результаты. Но большая часть их реализации одна и та же:

  • На mount добавьте прослушиватель изменений в DataSource.
  • Внутри слушателя вызывается setStateпри каждом изменении источника данных.
  • В режиме размонтирования удалите прослушиватель изменений.

Вы можете себе представить, что в большом приложении эта же схема подписки DataSource и вызова setState будет повторяться снова и снова. Нам нужна абстракция, которая позволяет нам определять эту логику в одном месте и делиться ею по многим компонентам. Именно здесь превосходят компоненты более высокого порядка.

Мы можем написать функцию, которая создает компоненты, такие как CommentList и BlogPost, которые подписываются DataSource. Функция будет принимать в качестве одного из своих аргументов дочерний компонент, который получает подписанные данные в качестве опоры. Назовем функцию withSubscription:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

Первый параметр - это завернутый компонент. Второй параметр извлекает интересующие нас данные, учитывая a DataSource и текущий реквизит.

Когда CommentListWithSubscription и BlogPostWithSubscription визуализируются, CommentList и BlogPost будет передана data поддержка с самыми последними данными, полученными из DataSource:

// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

Обратите внимание, что HOC не изменяет входной компонент и не использует наследование для копирования его поведения. Скорее, HOC составляет исходный компонент, обертывая его в компонент контейнера. HOC - это чистая функция с нулевыми побочными эффектами.

Вот и все! Обернутый компонент получает все опоры контейнера вместе с новой опорой data, которую он использует для рендеринга. HOC не имеет отношения к тому, как и почему используются данные, а обернутый компонент не имеет отношения к тому, откуда пришли данные.

Поскольку withSubscription это нормальная функция, вы можете добавить столько или несколько аргументов, сколько хотите. Например, вы можете захотеть присвоить имя data конфигурации prop, чтобы дополнительно изолировать HOC от упакованного компонента. Или вы можете принять аргумент, который настраивается shouldComponentUpdate, или тот, который настраивает источник данных. Все это возможно, поскольку HOC имеет полный контроль над тем, как компонент определен.

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

Не мутируйте исходный компонент. Используйте композицию.

Сопротивляйтесь соблазну модифицировать прототип компонента (или иным образом его мутировать) внутри HOC.

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // The fact that we're returning the original input is a hint that it has
  // been mutated.
  return InputComponent;
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);

Есть несколько проблем с этим. Первый заключается в том, что входной компонент нельзя повторно использовать отдельно от расширенного компонента. Более важно, если вы примените еще один HOC, EnhancedComponent который также будет мутировать componentWillReceiveProps, функции первого HOC будут отменены! Эта HOC также не будет работать с функциональными компонентами, которые не имеют методов жизненного цикла.

Мутирование HOC - это нечеткая абстракция - потребитель должен знать, как они реализуются, чтобы избежать конфликтов с другими HOC. Вместо мутации, HOCs должны использовать композицию, обернув входной компонент в компонент контейнера:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // Wraps the input component in a container, without mutating it. Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}

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

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

Конвенция: Передача несвязанных реквизитов через упакованный компонент

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

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

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

Это соглашение помогает обеспечить максимально гибкое и многоразовое использование HOC.

Конвенция: максимальная композиционная способность

Не все HOC выглядят одинаково. Иногда они принимают только один аргумент, завернутый компонент:

const NavbarWithRouter = withRouter(Navbar);

Обычно HOC принимают дополнительные аргументы. В этом примере из Relay объект конфигурации используется для указания зависимостей данных компонента:

const CommentWithRelay = Relay.createContainer(Comment, config);

Наиболее распространенная подпись для HOC выглядит следующим образом:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

Какие?! Если вы разобьете его, легче понять, что происходит.

// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is a HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);

Другими словами, connect это функция более высокого порядка, которая возвращает компонент более высокого порядка!

Эта форма может показаться запутанной или ненужной, но она имеет полезное свойство. Одинарные аргументы HOC, такие как возвращаемые connect функцией, имеют подпись Component => Component. Функции, тип вывода которых совпадает с типом ввода, очень легко составить вместе.

// Instead of doing this...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
  // These are both single-argument HOCs
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

(Это же свойство также позволяет использовать connectдругие HOC-устройства в стиле энхансера в качестве декораторов, экспериментальное предложение JavaScript).

Функция composeполезности обеспечивается многими сторонними библиотеками, включая lodash (as lodash.flowRight), Redux и Ramda .

Конвенция: оберните отображаемое имя для легкой отладки

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

Наиболее распространенный метод заключается в том, чтобы обернуть отображаемое имя обернутого компонента. Поэтому, если ваш компонент более высокого порядка именован withSubscription, и отображаемое имя обернутого компонента CommentList, используйте отображаемое имя WithSubscription(CommentList):

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

Предостережения

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

Не использовать HOC внутри метода визуализации

Удивительный алгоритм React (называемый сверкой) использует идентификатор компонента, чтобы определить, нужно ли обновлять существующее поддерево или выбросить его и установить новый. Если возвращаемый компонент renderидентичен ( ===) компоненту из предыдущего рендеринга, React рекурсивно обновляет поддерево, отличая его от нового. Если они не равны, предыдущее поддерево полностью размонтировано.

Обычно вам не нужно об этом думать. Но это важно для HOC, потому что это означает, что вы не можете применять HOC к компоненту в методе рендеринга компонента:

render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />;
}

Проблема здесь заключается не только в производительности - перекомпоновка компонента приводит к потере состояния этого компонента и всех его детей.

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

В тех редких случаях, когда вам нужно динамически применять HOC, вы также можете делать это внутри методов жизненного цикла компонента или его конструктора.

Статические методы должны быть скопированы

Иногда полезно определить статический метод для компонента React. Например, контейнеры Relay предоставляют статический метод getFragment для облегчения составления фрагментов GraphQL.

Однако, когда вы применяете HOC к компоненту, исходный компонент завернут компонентом контейнера. Это означает, что новый компонент не имеет каких-либо статических методов исходного компонента.

// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);

// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true

Чтобы решить эту проблему, вы можете скопировать методы в контейнер, прежде чем возвращать его:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

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

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

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

// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';

Refs не пройдены

Хотя соглашение о компонентах более высокого порядка должно проходить через все реквизиты к обернутому компоненту, это не работает для ссылок. Это потому, что ref на самом деле не является проприетским key, он специально используется Реактивом. Если вы добавите ref в элемент, компонент которого является результатом HOC, ref ссылается на экземпляр внешнего компонента контейнера, а не на завернутый компонент. Решением этой проблемы является использование React.forwardRefAPI.