High Order Component (HOC) is an advanced technique for reusability of components in React development. HOC is not a React API. It is a development mode based on React features.

Specifically, HOC is a method that takes a component as an argument and returns a new component

const EnhancedComponent = higherOrderComponent(WrappedComponent)
Copy the code

There are many uses of the React third-party ecosystem, such as Redux’s Connect method or react-Router’s Withrouter method.

For example

We have two components:

// 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>); }}Copy the code
// 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} />; }}Copy the code

Although they are two different components and have different needs for a DataSource, they have a lot in common:

  • Listen to the DataSource after the component renders
  • Call setState in the listener
  • Remove listeners while unmout

In large-scale engineering development, such similar codes often appear, so if there is a way to extract and reuse these similar codes, the maintainability and development efficiency of the project can bring significant improvement.

With HOC we can provide a method that takes no component and a different configuration between components as an argument, and then return a wrapped component as a result.

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} / >; }}; }Copy the code

We can then wrap the component by simply calling this method:

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

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

Note: In HOC we are not modifying the input component or extending the component through inheritance. HOC is a combination of ways to achieve the purpose of expanding components, a HOC should be a method without side effects.

In this example we extract the similar lifecycle methods of the two components and provide selectData as a parameter to allow the input component to select the data it wants. Because withSubscription is a pure method, it can be wrapped later if similar components are present, saving a lot of code duplication.

Do not modify the original component, use composition to extend functionality

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);
Copy the code

It is possible to extend components in this way, but there are some problems

  • If the InputComponent itself has onecomponentWillReceivePropsLifecycle method, then it will be overridden
  • Functional Component does not apply because it has no lifecycle methods

The way the original component is modified lacks abstraction, and the user must know how the method is implemented to avoid the problems mentioned above.

If we do it in a combination way, we can avoid these problems

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);
Copy the code

Convention: Unrelated props are passed to the original component

HOC components add props to extend functions of the original component. These props should not be passed to the original component (except, of course, that HOC components need to use the props specified by the original component).

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} / >
  );
}
Copy the code

ExtraProp is the props to be used in HOC components. The rest of the props we consider to be the props to be used by the original component. If it is common to both, you can pass it separately.

Convention: Wrap the display name of the component to facilitate debugging

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

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

Simply put, HOC components can be easily observed by the React DevTool by manually specifying the displayName

Convention: Do not call HOC in a Render method

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 />;
}
Copy the code

Each time enhance is called, a new class is returned. The React Diffing algorithm determines whether the components need to be rendered based on their characteristics. If the components are not (===) exactly the same, the components will be rendered again. The deployment diff is performed after the props is passed in, which is very costly to performance. And rerendering will lose all state and children of the previous component.

The React component uses props to change its display. There is no need to dynamically generate a component for each rendering. Theoretically, any parameters that need to be customized during rendering can be configured by specifying props in advance.

Static methods must be copied

Sometimes we attach helper methods to the class of a component. If we wrap the class as described above, the class will not have these static methods. In this case, we usually copy these static methods to the wrapped component for consistency.

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

This works if you know that the input component has static methods. If you need more extensibility, you can use the third party plug-in hoist non-react statics

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

ref

Ref is a special attribute in React, similar to a key, that is not props, so if we pass a ref on a HOC component and get the wrapped component instead of the original component, this might cause some problems.

The React. ForwardRef method has been added after React 16.3 to solve this problem

I’m Jocky, a front-end engineer who focuses on React techniques and in-depth analysis. React is definitely a well-designed and forward-thinking framework that will make you feel more sophisticated the further you learn it. Follow me for the latest React updates and the most in-depth React learning.See more articles here