Original link: reactjs.org/docs/higher…

The introduction

In React, HOC is a high-level technique for reusing component logic. Higher-order components themselves are not part of the React API. It is a pattern based on the React composite feature.

Specifically, a higher-order component is a function that takes a component as an argument and returns a new component.

const EnhancedComponent = higherOrderComponent(WrappedComponent);
Copy the code

A component converts props to UI, and a higher-order component converts one component to another.

Higher-order components are common in third-party libraries, such as Redux’s Connect and Relay’s createFragmentContainer.

In this section we’ll talk about why higher-order components are useful and how to build our own.

Use higher-order components to solve crosscutting concerns

Note: We previously recommended using mixins to solve crosscutting concerns. But now we know that mixins can cause more problems. Read more about why we’re ditching mixins and how to migrate already written components.

Components are the basic unit for React code reuse. But in practice you will find that traditional components do not directly fit certain patterns.

For example, you now have a CommentList component that receives an external data source to render comments:

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource"Are some global data sources comments: datasource.getComments ()}; }componentDidMountRegistration () {/ / change event listeners DataSource. AddChangeListener (enclosing handleChange); }componentWillUnmount() {/ / remove listener DataSource. RemoveChangeListener (enclosing handleChange); }handleChange() {// Update state this.setState({comments: DataSource. GetComments ()}); }render() {
    return( <div> {this.state.comments.map((comment) => ( <Comment comment={comment} key={comment.id} /> ))} </div> ); }}Copy the code

Then you write a component that subscribes to individual blog posts using a similar pattern:

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

CommentList and BlogPost are different — they call different methods on DataSource and render different results. But most of the implementation details are the same:

  • After the component is mounted, isDataSourceAdd the change listener;
  • Inside the listener, called when the data source changessetState;
  • Remove listeners when a component is uninstalled.

As you can imagine, in a large application, this behavior of subscribing to a DataSource and calling setState exists all the time. We want an abstract approach that can write logic in just one place and then share that logic with the components that need it. This is where higher-order components excel.

Let’s now create a function that creates CommentList and BlogPost and subscribs to a DataSource. This function will receive as one of its arguments a child component that will receive the subscription data as props. Now let’s call this function withSubscription:

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

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

The first parameter is the wrapped component. The second parameter returns the data we need based on our given DataSource and prop.

When rendering by CommentListWithSubscription and BlogPostWithSubscription CommentList and BlogPost will receive the calculated data as the data from the current DataSource prop:

// This function takes a component as an argument...functionwithSubscription(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() {/ /... Responsible for subscription related operations... DataSource.addChangeListener(this.handleChange); }componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

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

    render() {/ /... Render wrapped components with the latest data // Note that we pass other datareturn <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
Copy the code

Note that the higher-order component does not modify the input component, nor does inheritance copy its behavior. In contrast, higher-order components wrap the original component in a container component. A higher-order component should be a pure function with no side effects.

The wrapped component gets all the props it needs from the container component and also receives a prop data for rendering. Higher-order components don’t care how data is used, while wrapped components don’t care where the data comes from.

This is because withSubscription is a normal function, and you can add as many arguments as you want. Say you want the name of data Prop to be configurable. To further separate higher-order components from wrapped components. Or you can accept a shouldComponentUpdate parameter, or be able to configure the data source parameter. All of this is possible because higher-order components control how components are defined.

Just like components, withSubscription’s association with the wrapped component is completely props based. This relationship makes it easy to replace higher-order components, as long as you can provide the same props to the wrapped component. For example, this is useful when you change third-party libraries for data retrieval.

Do not modify the original component, use composition

Do not attempt to modify a component’s prototype in a higher-order component or otherwise modify it.

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps); }; // Returns the original array, indicating that it has been modifiedreturnInputComponent; } // EnhancedComponent will print the result on the console when it receives prop const EnhancedComponent =logProps(InputComponent);
Copy the code

There are a couple of problems here. One is that input components can no longer be used as they were before higher-order components were enhanced. More importantly, if you wrap EnhancedComponent in another higher-order component that can modify the EnhancedComponent, the functionality of the first higher-order component will be overwritten! This higher-order component cannot be applied to functional components that have no life cycle.

Modifying higher-order components of input components is a poor form of abstraction — callers must know how they are implemented to avoid collisions with other higher-order components.

Instead of modifying, higher-order components should use composition, wrapping input components in a container component:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {// Wrap the input component in the container component instead of trying to modify itreturn<WrappedComponent {... this.props} />; }}}Copy the code

The higher-level component above has the same functionality as the higher-level component that modifies the input component, but avoids potential conflicts. It works well with class components and function components. And because it is a pure function, it can be combined with other higher-order components, or even with itself.

Perhaps you’ve seen some similarities between higher-order components and container components. Container components are one of the strategies for separating high-level and low-level concerns. Container components manage transactions using subscriptions and state, and pass props to those components that need data. Higher-order components use container components as part of their implementation. Higher-order components can be thought of as parameterized container components.

Convention: Pass unwanted props to the wrapped component

Higher-order components add features to components. They can’t change the convention much by themselves. Typically, we want the components returned from higher-order components to have similar interfaces to the input components.

Higher-order components should pass through props that are independent of themselves. Most higher-order components contain render methods like the following:

render() {// Filter out additional props associated with higher-order components without passing them through. const { extraProp, ... passThroughProps } = this.props; // Inject props into the wrapped component. Usually these props were / / the state value or a function instance const injectedProp = someStateOrInstanceMethod; // Pass props to the wrapped componentreturn( <WrappedComponent injectedProp={injectedProp} {... passThroughProps} /> ); }Copy the code

This convention ensures that higher-order components are flexible and reusable.

Convention: Maximize composability

Not all higher-order components look the same. Sometimes higher-order components take only one argument: wrapped component:

const NavbarWithRouter = withRouter(Navbar);
Copy the code

Usually higher-order components receive additional parameters. In the following example about Relay, the extra parameter config object is used to declare the component’s data dependencies:

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

The most common higher-order component signatures are as follows:

// React Redux 'connect' const ConnectedComment = connect(commentSelector, commentActions)(CommentList);Copy the code

What the hell is this? But if you break it down, you get a clearer picture of how it works.

// Connect is a function that returns another function const enhance = connect(commentListSelector, commentListActions); // The function returned is a higher-order component that returns a component associated with the Redux store const ConnectedComment = enhance(CommentList);Copy the code

In other words, connect is a higher-order function that returns a higher-order component!

This form may seem confusing or unnecessary, but it has a very useful property. Just like the high-order single-argument Component returned by the connect function, it has a signature Component => Component. Functions whose input type is the same as their output type are very easy to combine.

// Don't do that... const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // ... You can compose a combination of the utility functions // compose(f, g, h) and... args) => f(g(h(... Args))) the same const enhance = compose(// they are both single-parameter high-order components withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)Copy the code

(The same property also allows Connect and other higher-level components to take on the role of decorator, an experimental proposal of JavaScript.)

Many third-party libraries provide compose tool functions, such as LoDash (LoDash.flowright),Redux, and Ramda.

Convention: Package shows the name all over for easy debugging

Container components created by higher-order components are displayed like any other component in React Developer Tools. For better debugging, select a display name to show that it is a component created by a higher-order component.

The most common method is to wrap the display name of the wrapped component. So if your higher-order component is called withSubscription and the display name for the wrapped component is CommentList, use withSubscription as the name:

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

Matters needing attention

Higher-order components have a few caveats that may not be easy for people new to React to understand.

Do not use higher-order components in render methods

The React diff algorithm uses component identifiers to determine whether to update the subcomponent tree or discard and remount the new subtree. If the render method returns the same component as the last rendered component (===),React will recursively update the subcomponent tree and the new subcomponent tree based on the diff algorithm. If they are not the same, the subcomponent tree will be completely uninstalled.

Normally, you don’t have to think about it. But this is important for higher-order components, because it means you can’t use higher-order components in the render method of the component to return components:

render() {// A new EnhancedComponent is created with each update // EnhancedComponent1! == EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // Doing so causes the entire sub-tree to be unmounted and remounted on each render!return <EnhancedComponent />;
}
Copy the code

This isn’t just a performance issue — remounting the component will result in the loss of its state and all of its child elements.

Conversely, if a higher-order component is called outside of the component, the component is created only once. After that, the component’s identity will remain consistent throughout the rendering process. That’s what we want.

Although this is rare, there are times when you need to use higher-order components on the fly. You can use higher-order components in component lifecycle methods or constructors.

Be sure to copy static methods

Sometimes it’s useful to define a static method in the React component. For example, the Relay container exposes a static getFragment method to facilitate composition of GraphQL fragments.

When you apply a higher-order component to a component, the original component is wrapped in the container component. But this means that the new component will not hold any of the original component’s static methods.

/ / define a static method WrappedComponent. StaticMethod =function() {/ *... */ / Now call a higher-order component const EnhancedComponent = enhance(WrappedComponent); / / new enhancements component is not a static method of typeof EnhancedComponent. StaticMethod = = ='undefined' // true
Copy the code

To solve this problem, you can copy these static methods to the container component before returning it:

functionenhance(WrappedComponent) { class Enhance extends React.Component {/*... * /} / / must want to know what needs to be replicated method Enhance staticMethod. = WrappedComponent staticMethod;return Enhance;
}
Copy the code

However, the above method requires that you know what methods to copy. You can use hoist non-react statics to automatically copy all non-React static methods:

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

An alternative is to export the static method again:

// Don't do that... MyComponent.someFunction = someFunction;exportdefault MyComponent; / /... Export methods separately...export{ someFunction }; / /... In the consumption module, introduce both import MyComponent, {someFunction} from'./MyComponent.js';
Copy the code

Refs are not passed through

Although the rule for higher-order components is to pass all props through to the wrapped component, an exception is made for refs. This is because refs are not really prop, just like keys, which are treated differently by React. If you add a REF to a component generated by a higher-order component, the ref refers to the outermost container component, not the wrapped component.

The solution is to use the React. ForwardRef API (introduced in React16.3) to learn more in Refs forwarding.