Подробное объяснение компонентов высшего порядка React HOC

внешний интерфейс алгоритм JavaScript React.js Redux

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

HOC — это метод, который принимает компонент в качестве параметра и возвращает новый компонент.

const EnhancedComponent = higherOrderComponent(WrappedComponent)

В сторонней экосистеме React есть много применений, например Reduxconnectметод или React-Router'swithrouterметод.

Например

У нас есть две составляющие:

// 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>
    );
  }
}
// BlogPost
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} />;
  }
}

Хотя это два разных компонента и предъявляются разные требования к DataSource, у них много общего:

  • Слушайте DataSource после рендеринга компонента
  • Вызов setState в прослушивателе
  • Удалить слушателя при отключении

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

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

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

Затем мы можем обернуть компонент, просто вызвав этот метод:

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

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

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

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

Не модифицируйте исходные компоненты, используйте композицию для функционального расширения

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

С помощью вышеуказанных методов мы также можем добиться эффекта расширения компонентов, но возникнут некоторые проблемы.

  • Если сам InputComponent также имеетcomponentWillReceivePropsМетод жизненного цикла, тогда он будет переопределен
  • функциональная составляющая не применяется, так как не существует методов жизненного цикла

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

Если мы будем делать это в комплексе, мы сможем избежать этих проблем.

function logProps(InputComponent) {
  return class extends React.Component{
    componentWillReceiveProps(nextProps) {
        console.log('Current props: ', this.props);
        console.log('Next props: ', nextProps);
    }
    render() {
        <InputComponent {...this.props} />
    }
  }
}

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

Соглашение: несвязанные реквизиты передаются исходному компоненту

Компонент 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}
    />
  );
}

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

Соглашение: оберните отображаемое имя компонента для облегчения отладки.

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

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

Проще говоря, вручную указавdisplayNameЧтобы сделать компоненты HOC более легкими для наблюдения с помощью реактивного devtool

Соглашение: не вызывайте метод 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 />;
}

один звонок каждый разenhanceТо, что возвращается, является новым классом. Алгоритм сравнения React определяет, нужно ли его повторно отображать в соответствии с характеристиками компонента. Если компоненты не (===) полностью равны при рендеринге дважды, они будут повторно отображаться напрямую. , а развертывание происходит на основе пропсов и далее выполняется diff, что имеет очень большую потерю производительности. И повторный рендеринг потеряет все состояние и дочерние элементы предыдущего компонента.

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

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

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

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

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

ref

ref — это специальное свойство в React — аналогично ключу, оно не относится к пропсам, то есть мы не передаем ref, передавая пропсы, тогда, если мы поместим ref на компонент HOC в это время, get is обернутый компонент вместо исходного компонента, что может вызвать некоторые проблемы.

Официальное дополнение было добавлено после React 16.3.React.forwardRefспособ решения этой проблемы, вы можете обратиться кздесь

Я Джоки, фронтенд-инженер, который фокусируется на навыках React и глубоком анализе React определенно является фреймворком, который чем больше вы изучаете, тем больше вы чувствуете, что его дизайн сложный и думает наперед. Подписывайтесь на меня, чтобы получать последние новости о React и самые глубокие знания о React.Больше статей здесь