Preface ✍ ️

  • As a member of the front-end team, I have never been exposed to React advanced components. When I first saw the name 😱, I was confused and even had the idea of “starting to give up”.
  • However, 🦒, after further study, is actually a very simple concept, but very common things. Its function is to achieve code reuse and logical abstraction, onstate ε’Œ propsAbstraction and manipulation, refinement of components (such as adding life cycles), rendering hijacking, etc.
  • Because of the availability of higher-order components πŸ’ͺ, it is frequently used in large quantitiesReact.jsRelated third-party libraries such asReact-Redux(Used to manage react application state, portal πŸ‘‰Redux + React-router introduction πŸ“– and configuration πŸ‘©πŸΎπŸ’» tutorial),React-Loadable(used to load higher-order components with dynamically imported components), etc.
  • Introduced so much, the following into the topic, through the introduction of the relevant basic knowledge and practice scenarios, take you to the high-level components πŸ›.

Basic concepts of higher-order components (what are they ❓)

  • HOC (higher-order Components) is not a component, but a function that takes a component as an argument and returns a modified new component:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
Copy the code
  • It is important to distinguish between components that willpropsA higher-order component is a component that is converted to another component.
  • High-order components are an advanced technique used in React to reuse component logic. See the official documentation for details on how this works.

Reasons for using higher-order components (why ❓)

  • In business development, although it is possible to complete project development without mastering higher-order components, if we can flexibly use higher-order components (plus πŸ₯°), the project code can become more elegant, while enhancing the reusability and flexibility of the code, and improving the development efficiency.
  • At the same time, understanding higher-order components helps us understand various kinds ofReact.jsThe principle of third-party libraries is very helpful πŸ‘.
  • The problems that higher-order components can solve can be summarized in the following three aspects:
    • Extract repetitive code to realize component reuse. Common scenario: page reuse.
    • Conditional rendering, controlling the rendering logic of components (render hijacking), common scenarios: permission control.
    • Capture/hijack the life cycle of the component being processed, common scenarios: component rendering performance tracking, logging.
  • It can be seen that the role of higher-order components is very powerful πŸ’ͺ. Next, I will introduce the implementation of higher-order components, so as to deepen our understanding of the role of higher-order components.

Implementation of higher-order components (how to do ❓)

  • Typically, higher-order components can be implemented in one of two ways:
    • Properties Proxy (Props Proxy)
      • Returns a stateless function component
      • Returns a class component
    • Inheritance Inversion
  • The differences in how higher-order components are implemented determine their respective application scenarios: oneReactThe component containsprops,state,ref, life cycle method,staticMethods andReactElement tree has several important parts, so I will compare the differences between the two higher-order component implementations in the following aspects:
    • Whether the original component can be wrapped
    • Whether the original component is inherited
    • Can read/manipulate the original componentprops
    • Can read/manipulate the original componentstate
    • Whether throughrefAccess to the original componentdomThe element
    • Whether it affects some of the life cycles of the original component
    • Whether to fetch the original componentstaticmethods
    • Can hijack the original component lifecycle method
    • Can render hijack

The property broker

  • Property brokering is the most common implementation, and it is essentially a composite approach that implements functionality by wrapping components in container components.
  • The life cycle relationship between high-order components implemented by property proxy and the original component is exactly the life cycle relationship between the React parent component. Therefore, high-order components implemented by property proxy will affect some life cycle methods of the original component.

Operating props

  • The simplest property broker implementation code is as follows:
// Return a stateless function componentfunction HOC(WrappedComponent) {
  const newProps = { type: 'HOC' };
  returnprops => <WrappedComponent {... props} {... newProps}/>; } // Return a stateful class componentfunction HOC(WrappedComponent) {
  return class extends React.Component {
    render() {
      const newProps = { type: 'HOC' };
      return<WrappedComponent {... this.props} {... newProps}/>; }}; }Copy the code
  • As you can see from the code above, a higher-order component wrapped by a property broker can intercept a component passed by its parentpropsIn advance topropsDo something like add onetypeProperties.

Abstract the state

  • Note ⚠️ that higher-order components implemented through property brokers cannot operate directly on the original componentstateBut it can passpropsAnd the callback functionstateAbstraction. ️
  • A common example is the transition from an uncontrolled component to a controlled component:
// Higher-order componentsfunction HOC(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        name: ' '}; this.onChange = this.onChange.bind(this); } onChange = (event) => { this.setState({ name: event.target.value, }) }render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onChange,
        },
      };
      return<WrappedComponent {... this.props} {... newProps} />; }}; } @hoc class Example extends Component {render() {
    return <input name="name" {...this.props.name} />;
  }
}
Copy the code

Get the REFS reference

  • In order to accessDOM element (focusEvents, animations, using third-party DOM manipulation libraries), sometimes we use componentsrefAttributes, aboutrefsPlease refer toThe official documentation.
  • refProperties can only be declared on components of class type, not on components of function type (because stateless components have no instances).
  • A higher-order component implemented by property proxy cannot directly obtain the original componentrefsReferences, however, can be passed in the original component’srefIn the callback function called by the parent componentrefCallback function to get the original component’srefsReferences.
  • Let’s say I have aUserComponent (original component), which has the following code:
import * as React from 'react';
import * as styles from './index.module.less'; interface IProps { name: string; age: number; inputRef? : any; } class User extends React.Component<IProps> { private inputElement: any ; staticsayHello () {
    console.error('hello world'); // tslint:disable-line
  }

  constructor (props: IProps) {
    super(props);
    this.focus = this.focus.bind(this);
    this.onChange = this.onChange.bind(this);
  }

  state = {
    name: ' ',
    age: 0,
  };

  componentDidMount () {
    this.setState({
      name: this.props.name,
      age: this.props.age,
    });
  }

  onChange = (e: any) => {
    this.setState({
      age: e.target.value,
    });
  }

  focus () {
    this.inputElement.focus();
  }

  render () {
    return(<div className={styles.wrapper}> <div className={styles.namewrapper}> Name: {this.state.name}</div> <div className={styles.ageWrapper}> Age: <input className={styles.input} value={this.state.age} onChange={this.onChange}type="number"
              ref={input => {
                if(this.props.inputRef) { this.props.inputRef(input); } this.inputelement = input;} this.inputelement = input; } /> </div> <div> <button className={styles.button} onClick={this.focus} > Get the focus of the input box </button> </div> </div>); }}export default User;
Copy the code
  • A property broker can retrieve the original componentrefsThe high-level component code referenced is as follows:
import * as React from 'react';
import * as styles from './index.module.less';

function HOC (WrappedComponent: any) {
    let inputElement: any = null;

    function handleClick () {
      inputElement.focus();
    }

    function wrappedComponentStaic () {
      WrappedComponent.sayHello();
    }

    return(props: any) => ( <div className={styles.hocWrapper}> <WrappedComponent inputRef={(el: any) => { inputElement = el; }} {... props} /> <inputtype="button"
          value="Get child component input box focus"
          onClick={handleClick}
          className={styles.focusButton}
        />
        <input
          type="button"
          value="Call the child static"
          onClick={wrappedComponentStaic}
          className={styles.callButton}
        />
      </div>
    );
}

export default HOC;
Copy the code
  • Use:
import React from 'react';
import HOC from '.. /.. /components/OperateRefsHOC';
import User from '.. /.. /components/User';

const EnhanceUser = HOC(User);

class OperateRefs extends React.Component<any> {
  render () {
    return <EnhanceUser name="Xiao Ming"age={12} />; }}export default OperateRefs;
Copy the code
  • After being wrapped by higher-order componentsEnhanceUserThe component can be accessedUserIn the componentinputChemical element:

Gets the static method of the original component

  • When the component to be processed is a class component, the higher-order component implemented through the property broker (whether it returns a function component or a class component) can obtain the static method of the original component, as shown in the code of the higher-order component above. The core code is as follows:
import * as React from 'react';
import * as styles from './index.module.less';

functionHOC (WrappedComponent: any) {/* omit irrelevant code... * /function wrappedComponentStaic () {
      WrappedComponent.sayHello();
    }

    return(props: any) => ( <div className={styles.hocWrapper}> <WrappedComponent inputRef={(el: any) => { inputElement = el; }} {... Props} /> /*... */ <inputtype="button"
          value="Call the child static"
          onClick={wrappedComponentStaic}
          className={styles.callButton}
        />
      </div>
    );
}

export default HOC;
Copy the code
  • The effect is as follows:

Conditional rendering is implemented using props

  • Higher-order components implemented through property brokers cannot directly implement rendering hijacking of the original component (i.e., inside the original component)renderThe control is not very strong) but can be passedpropsTo control whether to render and pass in data:
import * as React from 'react';
import * as styles from './index.module.less';

functionHOC (WrappedComponent: any) {/* omit irrelevant code... * /function wrappedComponentStaic () {
      WrappedComponent.sayHello();
    }

    return(props: any) => ( <div className={styles.hocWrapper}> { props.isShow ? ( <WrappedComponent {... Props} />) : <div> No data </div>} </div>); }export default HOC;
Copy the code

Wrap the incoming component with other elements

  • We can achieve layout or style purposes by wrapping the original components in ways like the following:
function withBackgroundColor(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ backgroundColor: '#ccc'}}> <WrappedComponent {... this.props} {... newProps} /> </div> ); }}; }Copy the code

Reverse inheritance

  • Reverse inheritance refers to using a function that takes a component passed in as an argument and returns a component of the class that inherits the componentrender()Method returnssuper.render()Method, the simplest implementation is as follows:
const HOC = (WrappedComponent) => {
  return class extends WrappedComponent {
    render() {
      returnsuper.render(); }}}Copy the code
  • In contrast to property proxy, high-order components implemented using reverse inheritance allow high-order components to passthisAccess to the original component, so you can directly read and manipulate the original component’sstate/ref/ Lifecycle approach.
  • Higher-order components implemented by reverse inheritance can passsuper.render()Method to get the object of the component instance passed inrenderResult, so you can render hijack the incoming component (the biggest feature), as in:
    • Conditionally present the element tree (element tree)
    • Operation byrender() The output of theReactElement tree
    • In anyrender()The output of theReactOperation in elementprops
    • Wrap the rendering result of the incoming component with other elements

Hijack the original component lifecycle method

  • Because a high-order component implemented in reverse inheritance returns a new component that inherits from the passed component, when the new component defines the same method, instance methods of the parent (passed component) are overridden, as shown in the following code:
functionHOC(WrappedComponent){// Inherits the incoming componentreturnClass HOC extends WrappedComponent {// Note: This overrides the componentDidMount methodcomponentDidMount(){
      ...
    }

    render(){// Use super to call the render method of the passed componentreturnsuper.render(); }}}Copy the code
  • Although lifecycle overrides are covered, we can hijack the lifecycle in other ways:
functionHOC(WrappedComponent){ const didMount = WrappedComponent.prototype.componentDidMount; // Inherits the incoming componentreturn class HOC extends WrappedComponent {
    componentDidMount(){// hijack the lifecycle of the WrappedComponentif(didMount) { didMount.apply(this); }... }render(){// Use super to call the render method of the passed componentreturnsuper.render(); }}}Copy the code

Read/manipulate the state of the original component

  • High-order components implemented in reverse inheritance can read, edit, and delete incoming component instancesstate, as shown in the following code:
functionHOC(WrappedComponent){ const didMount = WrappedComponent.prototype.componentDidMount; // Inherits the incoming componentreturn class HOC extends WrappedComponent {
    async componentDidMount() {if(didMount) { await didMount.apply(this); This.setstate ({number: 2}); }render(){// Use super to call the render method of the passed componentreturnsuper.render(); }}}Copy the code

Rendering hijacked

Conditions apply colours to a drawing
  • Conditional rendering means that we can decide whether or not to render a component based on some parameters (similar to property brokering), such as:
const HOC = (WrappedComponent) =>
  class extends WrappedComponent {
    render() {
      if (this.props.isRender) {
        return super.render();
      } else {
        return<div> Currently no data </div>; }}}Copy the code
Modify the React element tree
  • We can still get throughReact.cloneElementMethod modified byrenderThe React component tree output from the React method:
// Example from Inside the React Stackfunction HigherOrderComponent(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
      const newProps = {};
      if (tree && tree.type === 'input') {
        newProps.value = 'something here'; } const props = { ... tree.props, ... newProps, }; const newTree = React.cloneElement(tree, props, tree.props.children);returnnewTree; }}; }Copy the code

Property proxy versus reverse inheritance

  • The previous two sections describe higher-order components implemented by property broking and reverse inheritance, respectively:

    • Property brokering is done from a “composite” perspective, which facilitates manipulation from the outsideWrappedComponent, the objects that can be operated on arepropsOr, inWrappedComponentAdd some interceptors, controllers, etc.
    • Reverse inheritance operates from the inside from the perspective of inheritanceWrappedComponent, which can manipulate the inside of a componentstate, life cycle,renderFunctions and so on.
  • For the sake of comparison, the list of features for the higher-order components implemented in the two ways is as follows:

    Feature list The property broker Reverse inheritance
    Whether the original component can be wrapped Square root Square root
    Whether the original component is inherited x Square root
    Can read/manipulate the original componentprops Square root Square root
    Can read/manipulate the original componentstate δΉ„ Square root
    Whether throughrefAccess to the original componentdomThe element δΉ„ Square root
    Whether it affects some of the life cycles of the original component Square root Square root
    Whether to fetch the original componentstaticmethods Square root Square root
    Can hijack the original component lifecycle method x Square root
    Can render hijack δΉ„ Square root
  • As you can see, the high-order components implemented by reverse inheritance are more powerful and personalized than those implemented by property brokers, so they can adapt to more scenarios.

Specific practice πŸ’»

  • This section describes some practices of high-level components in business scenarios 🌰.

Page reuse

  • As mentioned earlier, the property broker is the most common high-level component implementation, which is essentially a composite approach to reuse component logic by wrapping components in container components. Therefore, if you want to reuse pages, you can use higher-order components implemented in the property broker way.
  • Let’s say we have in our projectpageA ε’Œ pageBThe two UIs interact with exactly the same movie list page, but because they belong to different movie categories, the data source and some of the copywriting are different, it might be written like this:
// views/PageA.js
import React from 'react';
import fetchMovieListByType from '.. /lib/utils';
import MovieList from '.. /components/MovieList';

class PageA extends React.Component {
  state = {
    movieList: [],
  }
  /* ... */
  async componentDidMount() {
    const movieList = await fetchMovieListByType('comedy');
    this.setState({
      movieList,
    });
  }
  render() {
    return <MovieList data={this.state.movieList} emptyTips="No comedy yet."/ >}}export default PageA;

// views/PageB.js
import React from 'react';
import fetchMovieListByType from '.. /lib/utils';
import MovieList from '.. /components/MovieList';

class PageB extends React.Component {
  state = {
    movieList: [],
  }
  // ...
  async componentDidMount() {
    const movieList = await fetchMovieListByType('action');
    this.setState({
      movieList,
    });
  }
  render() {
    return <MovieList data={this.state.movieList} emptyTips="No action movies yet."/ >}}export default PageB;
Copy the code
  • By observing that the code on both pages has a lot of the same code, you may at first think you can muddle through πŸ€¦β™€οΈ. However, with the development of the business, more and more types of movies need to be online. Every time a new page is written, some repeated code will be added, which is obviously unreasonable πŸ™…, so we need to extract the repeated logic in the page πŸ”¬ :
// HOC
import React from 'react';
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
  return class extends React.Component {
    async componentDidMount() {
      const data = await fetchingMethod();
      this.setState({
        data,
      });
    }
    
    render() {
      return( <WrappedComponent data={this.state.data} {... defaultProps} {... this.props} /> ); }} // use: // views/ pagea.js import React from'react';
import withFetchingHOC from '.. /hoc/withFetchingHOC';
import fetchMovieListByType from '.. /lib/utils';
import MovieList from '.. /components/MovieList';
const defaultProps = {emptyTips: 'No Comedy yet'}

export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps);

// views/PageB.js
import React from 'react';
import withFetchingHOC from '.. /hoc/withFetchingHOC';
import fetchMovieListByType from '.. /lib/utils';
import MovieList from '.. /components/MovieList';
const defaultProps = {emptyTips: 'No action movie yet'}

export default withFetchingHOC(MovieList, fetchMovieListByType('action'), defaultProps);;

// views/PageOthers.js
import React from 'react';
import withFetchingHOC from '.. /hoc/withFetchingHOC';
import fetchMovieListByType from '.. /lib/utils';
import MovieList from '.. /components/MovieList'; const defaultProps = {... }export default withFetchingHOC(MovieList, fetchMovieListByType('some-other-type'), defaultProps);
Copy the code
  • You can see the higher-order components designed abovewithFetchingHOC, the changed parts (components and methods of getting data) are pulled out and passed in, thus achieving page reuse.

Access control

  • Imagine a scenario where a new feature has recently come online, containing a series of newly developed pages. You need to add the whitelist function to some of these pages. If users who are not in the whitelist visit these pages, they will only be prompted by copywriting and do not display relevant service data. After one week (function acceptance is completed), the whitelist will be removed and all users will be available.
  • There are several conditions in the above scenario:
    • Multiple page authentication: The authentication code cannot be written repeatedly in the page component.
    • Not whitelisted users only copy prompt: authentication process before business data request;
    • Remove whitelist after a period of time: Authentication should be decoupled from services. Adding or removing authentication should minimize impact on the original logic.
  • Encapsulate the authentication process and use the conditional rendering feature of higher-order components. If authentication fails, relevant documents are displayed. If authentication succeeds, business components are rendered. Since both attribute brokering and reverse inheritance can implement conditional rendering, we will use the higher-order components of the simpler attribute brokering approach to solve the problem:
import React from 'react';
import { whiteListAuth } from '.. /lib/utils'; /** * Whitelisted permissions * @param WrappedComponent * @returns {AuthWrappedComponent} * @constructor */function AuthWrapper(WrappedComponent) {
  return class AuthWrappedComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        permissionDenied: -1,
      };
    }
    
    async componentDidMount() { try { await whiteListAuth(); This.setstate ({permissionDenied: 0,}); } catch (err) { this.setState({ permissionDenied: 1, }); }}render() {
      if (this.state.permissionDenied === -1) {
        returnnull; // Authentication interface request not completed}if (this.state.permissionDenied) {
        return<div> is coming soon, please look for ~</div>; }return<WrappedComponent {... this.props} />; }}}export default AuthWrapper;
Copy the code
  • For pages that require permission control, just pass the page component as a parameter to the higher-order componentAuthWrapperβœ….
  • Use of higher-order components to completely decouple authentication from transactions and avoid redundant business requests when authentication fails. Add/remove a small amount of code to add/remove controls of user whitelist. Logic of existing business components does not affect ✌✌️✌️️.

Component rendering performance tracking

  • The previous two examples used property brokers to implement higher-order components. This section introduces 🌰, which uses reverse inheritance to implement higher-order components to track component rendering performance.
  • As mentioned earlier ✍️, can high-order components implemented by reverse inheritance hijack the original component lifecycle method? Therefore, we can easily record the rendering time of a component by using this feature:
import React from 'react'; Class Home extends React.Component {render () {
    return (<h1>Hello World.</h1>);
  }
}

// HOC
function withTiming (WrappedComponent: any) {
  let start: number, end: number;

  return class extends WrappedComponent {
    constructor (props: any) {
      super(props);
      start = 0;
      end = 0;
    }
    componentWillMount () {
      if (super.componentWillMount) {
        super.componentWillMount();
      }
      start = +Date.now();
    }
    componentDidMount () {
      if (super.componentDidMount) {
        super.componentDidMount();
      }
      end = +Date.now();
      console.error(`${WrappedComponent.name}Component rendering time is${end - start} ms`);
    }
    render () {
      returnsuper.render(); }}; }export default withTiming(Home);
Copy the code
  • Results:

Extended Reading (Q & A)

Will hooks replace higher-order components?

  • Hook 是 16.8 the ReactThe new feature that allows us to write without writingclassIn case of usestateAnd so forthReactCharacteristics (aboutHookThe related introduction can be readThe official documentation).
  • HookThe advent of the Internet has made a lot of awkward writing easier, most typically it can be replacedclassMost of the functionality in the lifecycle is put together with more related logic rather than being scattered across lifecycle instance methods.
  • althoughHookIt solves a lot of problems, but that obviously doesn’t meanHookCan replace higher-order components, because they still have their own advantages:
    • Higher-order components can easily inject functionality into a base by external protocolsComponent, so it can be used to make plug-ins, such asreact-swipeable-viewsIn theautoPlayHigher-order components, stateful by injectionpropsInstead of writing code directly to the main library. forHookThe intermediate process must be strongly dependent on the target componentHookIt’s justHookClearly not designed to solve plug-in injection problems).
    • HookIt can be seen more as a complement to higher-order component solutions, filling in the parts that higher-order components are not good at.Hook“Makes code more compact and easier to doControllerOr the logic that requires cohesion.
    • At presentHookIt’s still early days (The React 16.8.0Before it was officially released.HookStable version), some third party libraries may not be compatible for the time beingHook.
  • ReactThe authorities have not yetclass 从 ReactTo remove,classThe component andHookIt can exist at the same time. Officials also recommend avoiding any “extensive refactoring,” after allHookIs a very new feature that can be used in new non-critical code if you likeHook.

Conclusion πŸ‘€

  • A higher-order component is not a component; it is a pure function that converts one component into another.
  • The main function of higher-order components is to achieve code reuse and logical abstractionstate ε’Œ propsAbstraction and manipulation, refinement of components (such as adding life cycles), rendering hijacking, etc. Rational use of higher-order components in actual business scenarios can improve development efficiency and code maintainability.
  • The availability of higher-order components πŸ’ͺ makes them frequently available in large quantitiesReact.jsRelated third-party libraries such asReact-ReduxtheconnectMethods,React-LoadableAnd so on are used to understand the higher-order components for our understanding of variousReact.jsThe principle of third-party libraries is very helpful πŸ‘.
  • Higher-order components can be implemented in two ways, namely property proxy and reverse inheritance. It can be seen as a decorator pattern inReactImplementation: Implements enhancements to component functionality without modifying the original component.
  • For details, see πŸ‘‰ : React + typescript project customization process.

If there are any omissions in the above content, please leave a message ✍️ to point out, and progress together πŸ’ͺπŸ’ͺπŸ’ͺ

If you find this article helpful, πŸ€πŸ€ leave your precious πŸ‘

The resources

  1. Advanced Components (official documentation)
  2. The React high-order component takes three questions to heart
  3. React Goes from Mixin to HOC to Hook
  4. React Hooks on HoC and Render Props
  5. Do you really have React Hooks right?
  6. Getting started with ECMAScript 6 — Decorators