High-level component HOC

HOC is an advanced technique in React that reuses component logic. But the higher-order components themselves are not the React API. It’s just a pattern, and that pattern is necessarily generated by the combinatorial nature of React itself.

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

grammar

const EnhancedComponent = higherOrderComponent(WrappedComponent);
Copy the code

Usually we write contrast components, so what is a contrast component? A component converts the props property to UI, and a higher-order component converts one component to another.

Application scenarios

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

What is the meaning

Previously, mixins were used to address crosscutting concerns. But mixins create more problems than value. So remove mixins and find out more about how to convert components that you already use mixins. Obviously, crosscutting concerns are addressed using HOC.

The sample

1. Suppose there is a CommentList component that subscribs to and renders data from an external data source

// CommentList.js
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

2. Then, there is a component that subscribes to individual blog posts (BlogPost)

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

  conponentDidMount() {
    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

3. Above, the comments component CommentList differs from the article subscription component BlogPost in the following ways

  • callDataSourceIn different ways
  • Render output is different

However, they have something in common

  • When mounting a component toDataSourceAdd a listener that changes;
  • Within the listener, when the data source changes, it is calledsetState;
  • Remove change listeners when uninstalling components.

In a large application, the pattern of subscribing to data from a DataSource and calling setState will be used many times, when the front end will sniff out the code to tidy up, be able to extract the same place as an abstraction, and then many components can share it, which is the background for higher-order components.

4. We use a function withSubscription to allow it to do the following

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

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

The first argument to the withSubscription function above is the two components we wrote earlier, and the second argument retrieves the required data (DataSource and props).

What about this withSubscription function?

const withSubscription = (TargetComponent, selectData) = > {
  return class extends React.Component {
    constructor(props){
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

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

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

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

    render(){
      return <TargetComponent data={this.state.data} {. this.props} / >}}}Copy the code

5. Under the summary

  • A higher-order component neither modifies the parameter component nor copies its behavior using inheritance, and is simply a pure function with no side effects.
  • The wrapped component receives all of the containerpropsProperties and new datadataFor rendering output. Higher-order components don’t care about how the data is used, and wrapped components don’t care about where the data comes from.
  • The contract for higher order components and wrapped components ispropsProperties. This is where you can replace another higher-order component as long as they provide the samepropsAttribute to the wrapped component. You can think of higher-level components as a set of thematic skins.

Instead of changing the original component, use composition

Now, we have a preliminary understanding of higher-order components, but in the actual business, when we write higher-order components, it is easy to write and modify the content of components, must resist the temptation. Such as

const logProps = (WrappedComponent) = > {
  WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('CurrentProps'.this.props);
    console.log('NextProps', nextProps);
  }
  return WrappedComponent;
}

const EnhancedComponent = logProps(WrappedComponent);
Copy the code

Higher order componentslogPropsThere are a couple of questions

  • Wrapped componentWrappedComponentCannot be reused independently of enhanced components.
  • If you are in theEnhancedComponentApply another higher-order component tologProps2And will change as wellcomponentWillReceiveProps, high order componentslogProps“Will be overwritten.
  • Such higher-order components are not effective against functional components that have no life cycle

Aim at above problem, want to achieve same effect, can use combination way

const 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 a container, do not modify it, nice!
      return <WrappedComponent {. this.props} / >; }}}Copy the code

lenovo

If you haven’t noticed, high-level components and container component patterns have something in common.

  • Container components focus on passing as part of a strategy for separating responsibilities between the upper and lower levelspropsAttribute to child components;
  • Higher-order components, which use containers as part of their implementation, are understood to be parameterized container component definitions.

Convention: Pass unrelated props properties to wrapped components throughout

The higher-order component returns a component that has a similar interface to the wrapped component.

render(){
  // Filter out the props property specific to this higher-order component, and discard the extraProps
  const{ extraProps, ... restProps } =this.props;

  Inject injectedProps properties into the wrapped component. These are generally state values or instance methods
  const injectedProps = {
    // someStateOrInstanceMethod
  };

  return (
    <WrappedComponent injectedProps={injectedProps} {. restProps} / >)}Copy the code

Conventions help ensure maximum flexibility and reusability for higher-order components.

Convention: Maximized combinativity

Not all higher-order components look the same. Sometimes they only receive a single argument, the wrapped component:

const NavbarWithRouter = withRouter(Navbar);
Copy the code

In general, higher-order components receive additional parameters. In the following example from Relay, a config object is used to specify the component’s data dependencies:

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

The most common signatures of higher-order components are as follows:

const ConnectedComment = connect(commentSelector, commentActions)(Comment);
Copy the code

One way to think about it

// connect returns a function (higher-order component)
const enhanced = connect(commentSelector, commentActions);
const ConnectedComment = enhanced(Comment);
Copy the code

In other words, connect is a higher-order function that returns higher-order components! This form is somewhat confusing, but it has the property that a high-order function with only one argument (returned by connect) returns Component => Component, which allows functions of the same input and output types to be grouped together, together, together

/ / the pattern
const EnhancedComponent = withRouter(connect(commentSelector, commentActions)(Comment));

// Correct mode
// You can use a function composition tool
// compose(f, g, h) and (... args) => f(g(h(... Args))) is the same
const enhanced = compose(
  withRouter,
  connect(commentSelector, commentActions)
);
const EnhancedComponent = enhanced(Comment);
Copy the code

Many third-party libraries, including LoDash (e.g. Lodash.flowright), Redux, and Ramda, provide functions similar to compose’s functionality.

Convention: The wrapper displays the name for easy debugging

If your high-order component name is withSubscription and the display name of the wrapped component is CommentList, then the display name is withSubscription:

const withSubscription = (WrappedComponent) = > {
  // return class extends React.Component { /* ... */ };

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

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

Here are a few don ‘ts

Do not use higher-order components within the Render method

**React’s difference algorithm (called coordination) ** uses component identifiers to determine whether to update an existing subtree or throw it away and remount a new one. If the render method returns exactly the same component as the previous render returned (===), React updates the subtree recursively by differentiating it from the new one. If they are not equal, the previous subtree is completely unloaded.

In general, you don’t have to think about the difference algorithm. But it has to do with higher order functions. Because it means you can’t apply higher-order functions to a component within its render method:

render() {
  // Each render creates a new EnhancedComponent version
  // EnhancedComponent1 ! == EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // causes the subobject tree to be completely unloaded/reloaded each time
  return <EnhancedComponent />;
}
Copy the code

The above code can cause problems

  • Performance issues
  • Reloading a component causes the state of the original component and all of its children to be lost

Static methods must be copied

Problem: When you apply a higher-order component to a component, however, the original component is wrapped in a container component, which means that the new component does not have any static methods of the original component.

// Define static methods
WrappedComponent.staticMethod = function() {/ *... * /}
// Use higher-order components
const EnhancedComponent = enhance(WrappedComponent);

// Enhanced components have no static methods
typeof EnhancedComponent.staticMethod === 'undefined' // true
Copy the code

Solution: (1) You can copy the methods of the original component to the container

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

(2) To do so, you need to know exactly what static methods you need to copy. You can use the hoist non-react statics for auto-handling, which automatically copies all non-React static methods:

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

(3) Another possible solution is to export the component’s own static methods separately.

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

refsAttributes cannot be passed through

In general, higher-order components can pass all props properties to wrapped components, but not refs references. Because refs is not a pseudo-property like key, React makes it special. If you add ref to the element of a component created by a higher-order component, ref points to the outermost container component instance, not the wrapped component.

React16 provides the React. ForwardRef API to address this issue, as described in the Refs transfer section.

You can also

React source code

React source code parsing overview