Spec-Zone.ru › React 17

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

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

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

const EnhancedComponent = higherOrderComponent(WrappedComponent);

В то время как компонент преобразует свойства в пользовательский интерфейс, компонент высшего порядка преобразует компонент в другой компонент.

HOCы часто используются в сторонних библиотеках React, таких как connect Redux и createFragmentContainer Relay.

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

Использование 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, и они отображают различный вывод. Но большая часть их реализации одинакова:

  • При подключении добавьте обработчик изменений в 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)
);

Первый параметр — обернутый компонент. Второй параметр получает данные, которые нас интересуют, заданный 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 настраиваемым, чтобы ещё больше изолировать HOC от обернутого компонента. Или вы можете принять аргумент, который настраивает shouldComponentUpdate, или один, который настраивает источник данных. Всё это возможно, потому что HOC полностью управляет тем, как определён компонент.

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

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

Избегайте соблазна изменять прототип компонента (или иначе изменять его) внутри HOC.

function logProps(InputComponent) {
  InputComponent.prototype.componentDidUpdate = function(prevProps) {
    console.log('Current props: ', this.props);
    console.log('Previous props: ', prevProps);
  };
  // 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, который также изменяет componentDidUpdate, функциональность первого HOC будет перекрыта! Этот HOC также не будет работать с функциональными компонентами, у которых нет методов жизненного цикла.

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

Вместо изменения HOCы должны использовать композицию, обертывая входной компонент в компонент-контейнер:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props);
      console.log('Previous props: ', prevProps);
    }
    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 (как lodash.flowRight), Redux и Ramda.

Соглашение: Оберните имя отображения для удобства отладки

Компоненты-контейнеры, созданные HOC, отображаются в React Developer Tools, как и любой другой компонент. Для удобства отладки выберите имя отображения, которое указывает, что это результат 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ы внутри метода render

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

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

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;
}

Однако, это требует знания о том, какие именно методы необходимо скопировать. Можно использовать hoist-non-react-statics, чтобы автоматически скопировать все статические методы, не относящиеся к React:

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';

Свойства ref не передаются

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

Решение этой проблемы заключается в использовании API React.forwardRef (введённого в React 16.3). Узнайте больше об этом в разделе перенаправления ссылок.

Полезен ли этот раздел?

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/higher-order-components.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API