Reference links:

Juejin. Cn/post / 684490…

Juejin. Cn/post / 684490…

preface

High-order components and custom hooks are two popular solutions for reusing state logic in React

1. What are the higher-order components

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

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.

Simple examples of HOC:

//HOC
function visible(WrappedComponent) {
  return class extends Component {
    render() {
      const{ visible, ... props } =this.props;
      if (visible === false) return null;
      return <WrappedComponent {. props} / >; }}}// Wrap components with HOC
class Example extends Component {
  render() {
    return <span>The sample components</span>; }}export default HOC(Example)
// Or use a decorator
@HOC
class Example extends Component {
   render() {
    return <span>The sample components</span>; }}export default Example

/ / use
<Example visible={false} / >Copy the code

The above code is a simple application of HOC. The function takes a component as an argument and returns a new component. The new component can take a visible props and decide whether to render the incoming component based on the visible value.

2. Implementation of higher-order components

2.1 Attribute Proxy

You pass a React component as an argument to a function that returns a custom component. The Render function of this custom component returns the React component passed in.

This allows you to prop up and manipulate the functions of the incoming React component and determine how to render it. In fact, this generates higher-order components that are the parent of the original component. The function visible above is an implementation of a HOC property proxy.

In this way, the lifecycle call order of the HOC container component and the incoming component is consistent with the lifecycle order of the parent and child components. Similar to a stack call (first in, last out)

function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {. this.props} / >; }}}// An example
class Example extends Component {
  render() {
    return <input name="name" {. this.props.name} / >; }}export default HOC(Example)
Copy the code

HOC implemented through a property broker can have the following functions:

(1) Operation props

You can add, modify, delete, or perform special operations on the props of the component that is passed in.

Note that the component wrapped with HOC is actually passed to the props in the HOC container component. If you do not need to operate the props, make sure that the props are passed to the passed component in the container component. Otherwise, the passed component will not receive the props

function proxyHOC(WrappedComponent) {
  return class Container extends Component {
    render() {
      constnewProps = { ... this.props,user: "ConardLi"
      }
      return <WrappedComponent {. newProps} / >; }}}Copy the code

(2) Get the refS reference

The higher order component can get the ref of the component passed in, and get the instance of the component from ref (that is, get the this of the component instance passed in), as shown in the following code. When the program is initialized, the log method of the original component is called.

function refHOC(WrappedComponent) {
  return class Container extends Component {
    componentDidMount() {
      this.wapperRef.log()
    }
    render() {
      return <WrappedComponent {. this.props} ref={ref= > { this.wapperRef = ref }} />; }}}Copy the code

Note: By default, a component wrapped in HOC cannot get a reference to the original component’s refs when called externally

Although the convention for higher-order components is to pass all props to the wrapped component, this does not apply to refs. That’s because ref is not really a prop. Just like keys, it’s handled specifically by React. If you add ref to HOC’s return component, the REF reference points to the container component, not the wrapped component.

The solution to this problem is to use the React. ForwardRef API (introduced in React 16.3).

(3) Abstract state

// Higher-order components
function HOC(WrappedComponent) {
  return class Container 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} / >; }}; }/ / use
class Example extends Component {
  render() {
    return <input name="name" {. this.props.name} / >; }}export default HOC(Example)
/ / or
@HOC
class Example extends Component {
  render() {
    return <input name="name" {. this.props.name} / >; }}Copy the code

In this example, we override the name prop in the Input component by redefining it in the higher-order component (replacing it with value and onChange), effectively abstracting the same state operation. Changes the Input component from an uncontrolled component to a controlled component

(4) Static methods for manipulating components

You can add, modify, and delete static methods that are passed in to a component by making a fetch call

function refHOC(WrappedComponent) {
  return class Container extends Component {
    componentDidMount() {
    // Get the static method
      console.log(WrappedComponent.staticMethod)
    }
    // Add a static method
    WrappedComponent.addMethod1=() = >{}
    
    render() {
      return <WrappedComponent {. this.props} ref={ref= > { this.wapperRef = ref }} />; }}}Copy the code

But when you apply HOC to a component, the original component will be wrapped with a container component. This means that the container component does not pass in any static methods of the component by default, that is, it cannot get its static methods when it is imported elsewhere. So as with props, be sure to copy the static methods passed to the component onto the container component

(5) Implement conditional rendering according to props

Determine whether the incoming component is rendered based on the specific props (as in the basic HOC example above)

function visibleHOC(WrappedComponent) {
  return class extends Component {
    render() {
      if (this.props.visible === false) return null;
      return <WrappedComponent {. props} / >; }}}Copy the code

(6) Wrap incoming components with other elements

Rewrap the original component with other elements in the container component of HOC to achieve the purpose of layout or style modification:

function withBackgroundColor(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ backgroundColor: "#ccc}} ">
                    <WrappedComponent {. this.props} {. newProps} / >
                </div>); }}; }Copy the code

2.2 Reverse Inheritance

Returns a component that inherits the passed component and calls render of the original component in Render.

Because it inherits from the original component, it can access the lifecycle, props, state, render, and so on of the original component, and it can manipulate more properties than the property proxy.

In this way, the lifecycle of HOC and incoming components is called in a similar order to that of queues (First in first out)

function inheritHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return super.render(); }}}Copy the code

HOC, implemented through reverse inheritance, has the following additional capabilities compared to attribute brokers:

(1) Rendering hijacking

Render hijacking is where higher-order components can control the WrappedComponent rendering process and render various results. We can read, add, modify, or delete props from any React output, or read or modify the React tree, or conditionally display the tree, or wrap the tree with style controls.

Conditional rendering, mentioned in the property broker above, is also an implementation of render hijacking.

If the element tree contains a React component of function type, you cannot operate on its children

Render hijacking example in action:

function hijackHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const tree = super.render();
      let newProps = {};
      if (tree && tree.type === "input") {
        newProps = { value: "Rendering has been hijacked." };
      }
      const props = Object.assign({}, tree.props, newProps);
      const newTree = React.cloneElement(tree, props, tree.props.children);
      returnnewTree; }}}Copy the code

(2) Hijack the incoming component lifecycle

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:

function HOC(WrappedComponent){
  // Inherits the incoming component
  return class HOC extends WrappedComponent {
    // Note: The componentDidMount method will be overridden here
    componentDidMount(){... }render(){
      // Use super to call the render method of the incoming component
      return super.render(); }}}Copy the code

(3) Operation passed component state

High-order components implemented in reverse inheritance can read, edit, and delete state from an incoming component instance, as shown in the following code:

function debugHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      console.log("props".this.props);
      console.log("state".this.state);
      return (
        <div className="debuging">
          {super.render()}
        </div>)}}}Copy the code

Manipulating the state of the passed component can mess up the internal state of the WrappedComponent. Most higher-order components should restrict reading or increase state, especially the latter, by renaming state to prevent confusion.

2.3 Comparison between the two methods

  • Property brokers are from a “composite” perspective, which makes it easier to manipulate wrappedComponents externally, either as props, or by adding interceptors, controllers, etc.
  • Reverse inheritance operates from the inside of the WrappedComponent from an “inheritance” perspective, i.e. you can manipulate the state, lifecycle, render functions, and so on inside the component.

3. Practical application of higher-order components

(1) Logic reuse

Multiple page components have similar code structure and requirements, but some parameters and data are different, and there is more repetitive code. Use higher-order components for unified package encapsulation

Here are two page components with similar structure and requirements:

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

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

To extract the repeated logic into a HOC:

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

Example:

/ / 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 movies 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";
constdefaultProps = {... }export default withFetchingHOC(MovieList, fetchMovieListByType("some-other-type"), defaultProps);

Copy the code

The higher-order component withFetchingHOC designed above extracts different parts (components and methods of obtaining data) to the outside as incoming, thus realizing the reuse of pages.

(2) Permission control

function auth(WrappedComponent) {
  return class extends Component {
    render() {
      const { visible, auth, display = null. props } =this.props;
      if (visible === false || (auth && authList.indexOf(auth) === -1)) {
        return display
      }
      return <WrappedComponent {. props} / >; }}}Copy the code

AuthList is the list of all permissions that we request from the back end when we enter the program, and when the required permissions for the component are not in the list of incoming permissions, or when visible is set to False, we display them as the incoming component style, or null. We can apply HOC to any component that requires permission verification:

  @auth
  class Input extends Component {... } @authclass Button extends Component {... } <Button auth="user/addUser"> Add user </Button><Input auth="user/search" visible={false} >Add user</Input>

Copy the code

4. Other tips:

(1) Higher-order component parameters

Sometimes we need to pass in some parameters when we call higher-order components, which can be done in a very simple way:


import React, { Component } from "React"; 
function HOCFactoryFactory(. params) { 
 // There are things you can do to change params
 return function HOCFactory(WrappedComponent) { 
	 return class HOC extends Component { 
	 render() { 
	 return <WrappedComponent {. this.props} / >; }}}}Copy the code

When you use it, you can write:

HOCFactoryFactory(params)(WrappedComponent) 
/ / or
@HOCFatoryFactory(params) 
class WrappedComponent extends React.Component{}
Copy the code

(2) Higher-order component naming

When wrapping a higher-order component, we lose the displayName of the original WrappedComponent, which is an important property for development and debugging

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)}) `; 
/ / or
class HOC extends.{ 
 static displayName = `HOC(${getDisplayName(WrappedComponent)}) `; .Copy the code

You can then retrieve it via HOC. DisplayName