What are higher-order components

Before explaining what a higher-order component is, it is useful to know what a higher-order function is, because the concepts are very similar. Here is the definition of a higher-order function:

A higher-order function is one that takes one or more functions as arguments or returns a function.

Here is a simple higher-order function:

function withGreeting(greeting = () => {}) {
    return greeting;
}
Copy the code

Higher-order components are defined very similarly to higher-order functions:

A function is called a higher-order component if it takes one or more components as arguments and returns a component.

Here is a simple higher-order component:

function HigherOrderComponent(WrappedComponent) {
    return <WrappedComponent />;
}
Copy the code

So you might find that a high-order Component that returns a Stateless Component is actually a high-order function because a Stateless Component is itself a pure function.

Stateless components are also called functional components.

Higher-order components in React

High-order components in React come in two main forms: property proxy and reverse inheritance.

Properties Proxy (Props Proxy)

The simplest property broker implementation:

/ / a statelessfunction HigherOrderComponent(WrappedComponent) {
    returnprops => <WrappedComponent {... props} />; } // or // statefulfunction HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;

        }

    };
}
Copy the code

As you can see, the property proxy is basically a function that takes a WrappedComponent as an argument and returns a class that inherits the React.component.component.class. And returns the WrappedComponent passed in in the Render () method of the class.

So what can we do with the higher-order components of the property broker type?

Because the high-order component of the property broker type returns a standard react.component. React component can do what the high-order component of the property broker type can do, for example:

  • Operating props
  • Out of the state
  • Access the component instance through ref
  • Wrap the incoming WrappedComponent with other elements

Operating props

Add a new attribute to WrappedComponent:

function HigherOrderComponent(WrappedComponent) {

    return class extends React.Component {

        render() {

            const newProps = {

                name: 'Big chestnut',
                age: 18,
            };
            return<WrappedComponent {... this.props} {... newProps} />; }}; }Copy the code

Out of the state

To extract state, use the props and callback functions:

function withOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                name: ' '}; } onChange = () => { this.setState({ name:'Big chestnut'}); }render() {
            const newProps = {
                name: {
                    value: this.state.name,
                    onChange: this.onChange,
                },
            };
            return<WrappedComponent {... this.props} {... newProps} />; }}; }Copy the code

How to use:

const NameInput = props => (<input name="name"{... props.name} />);export default withOnChange(NameInput);
Copy the code

This converts the input into a controlled component.

Access the component instance through ref

The ref attribute of a component is sometimes used when you need to access a DOM Element using a third-party DOM manipulation library. It can only be declared on components of type Class, not function (stateless).

The value of ref can be a string (not recommended) or a callback function, in which case it is executed when:

  • The callback function is executed immediately after the component is mounted (componentDidMount). The callback function takes an instance of the component.
  • When the component is unmounted (componentDidUnmount) or the original ref property itself changes, the callback function is immediately executed, and the callback function parameter is null.

How do I get an instance of a WrappedComponent in a higher-order component? WrappedComponent’s ref attribute, which executes the ref callback on component componentDidMount and passes in an instance of the component:

function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        executeInstanceMethod = (wrappedComponentInstance) => {
            wrappedComponentInstance.someMethod();
        }

        render() {
            return <WrappedComponent {...this.props} ref={this.executeInstanceMethod} />;
        }
    };
}
Copy the code

Note: You cannot use the REF attribute on stateless components (function type components) because stateless components have no instances.

Wrap the incoming WrappedComponent with other elements

Wrap the WrappedComponent with a div element with a background color of #fafafa:

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

Inheritance Inversion

The simplest implementation of reverse inheritance:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            returnsuper.render(); }}; }Copy the code

Reverse inheritance is a function that takes a WrappedComponent as an argument, returns a class that inherits the WrappedComponent, and returns the super.render() method in the render() method of that class.

You’ll find that the property proxy and reverse inheritance implementations are somewhat similar in that they both return a subclass that inherits from a parent class, except that the property proxy inherits from react.component. and the reverse inheritance inherits from the passed WrappedComponent.

What reverse inheritance can be used for:

  • The operating state
  • Render Highjacking

The operating state

Higher-order components can read, edit, and delete states in WrappedComponent instances. You can even add more states, but this is not recommended because it can make state difficult to maintain and manage.

function withLogging(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return (
                <div>
                    <h2>Debugger Component Logging...</h2>
                    <p>state:</p>
                    <pre>{JSON.stringify(this.state, null, 4)}</pre>
                    <p>props:</p>
                    <pre>{JSON.stringify(this.props, null, 4)}</pre>
                    {super.render()}
                </div>
            );
        }
    };
}
Copy the code

In this example, we nested additional elements of the WrappedComponent to print both the state and props of the WrappedComponent, using the higher-order functions that can read the state and props.

Render Highjacking

It is called render hijacking because higher-order components control the render output of WrappedComponent components. With render hijacking we can:

  • Conditionally display element tree
  • Operates the React element tree output by Render ()
  • Operate props in any React element output by render()
  • Wrap the incoming component WrappedComponent with other elements (same as the property proxy)
Conditions apply colours to a drawing

Determine which component to render using the props. IsLoading condition.

function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                returnsuper.render(); }}}; }Copy the code
Operates the React element tree output by Render ()
function 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

Problems with higher-order components

  • Static method loss
  • The refs attribute cannot be transparently transmitted
  • Reverse inheritance does not guarantee that the full tree of subcomponents will be parsed

Static method loss

Because the original component is wrapped in a container component, this means that the new component does not have any static methods of the original component:

/ / define a static method WrappedComponent staticMethod =function() {} // Use highenhancedComponent const EnhancedComponent = HigherOrderComponent(WrappedComponent); . / / enhanced component has a static method typeof EnhancedComponent staticMethod = = ='undefined' // true
Copy the code

So we must copy the static method:

functionHigherOrderComponent(WrappedComponent) {class extends React.Component {} // You must know the method to copy to Enhance. StaticMethod = WrappedComponent.staticMethod;return Enhance;
}
Copy the code

The React community implements a library called hoist-non-react-statics that automatically copies all non-React static methods:

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

The refs attribute cannot be transparently transmitted

Generally speaking, higher-order components can pass all props to the WrappedComponent WrappedComponent, but one attribute that cannot be passed is ref. Unlike other properties, React treats them differently.

If you add a ref reference to an element of a component created by a higher-order component, the ref points to the outermost container component instance, not the WrappedComponent wrapped in it.

React provides an API called the React. ForwardRef (added in React 16.3) :

function withLogging(WrappedComponent) {
    class Enhance extends WrappedComponent {
        componentWillReceiveProps() {
            console.log('Current props', this.props);
            console.log('Next props', nextProps);
        }

        render() { const {forwardedRef, ... rest} = this.props; // assign the forwardedRef to refreturn<WrappedComponent {... rest} ref={forwardedRef} />; }}; // the React. ForwardRef method passes props and ref to the callback functionfunction forwardRef(props, ref) {
        return<Enhance {... props} forwardRef={ref} /> }return React.forwardRef(forwardRef);
}

const EnhancedComponent = withLogging(SomeComponent);
Copy the code

Reverse inheritance does not guarantee that the full tree of subcomponents will be parsed

The React component comes in two forms, class type and function type (stateless component).

We know that reverse inherited render hijacking controls the WrappedComponent rendering process, which means we can do various things to the results of elements Tree, state, props, or render().

However, if the rendering elements Tree contains a component of type function, then the child components of the component cannot be manipulated.

Conventions for higher-order components

While higher-order components bring us great convenience, we also need to follow a few conventions:

  • Props stay consistent
  • You cannot use the ref attribute on a functional (stateless) component because it has no instance
  • Do not change the original WrappedComponent in any way
  • Pass through the unrelated props property to the WrappedComponent WrappedComponent
  • Don’t use higher-order components in the Render () method
  • Compose higher-order components with Compose
  • The wrapper shows the name for easy debugging

Props stay consistent

A higher-order component needs to keep the props of the original component intact while adding features to its child components. That is, the passed component needs to be the same as the returned component needs to be.

You cannot use the ref attribute on a functional (stateless) component because it has no instance

The functional component itself has no instance, so the ref attribute does not exist.

Do not change the original WrappedComponent in any way

Instead of modifying a component’s prototype in any way within higher-order components, consider the following code:

function withLogging(WrappedComponent) {

    WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
        console.log('Current props', this.props);
        console.log('Next props', nextProps);
    }
    return WrappedComponent;
}

const EnhancedComponent = withLogging(SomeComponent);
Copy the code

The WrappedComponent is modified inside the higher-order component. Once the original component is modified, the reuse of the component is lost, so return the new component via a pure function (the same input always has the same output) :

function withLogging(WrappedComponent) {
    return class extends React.Component {
        componentWillReceiveProps() {
            console.log('Current props', this.props);
            console.log('Next props', nextProps);
        }

        render() {// Pass through the argument without modifying itreturn <WrappedComponent {...this.props} />;
        }
    };
}
Copy the code

This optimized withLogging is a pure function and does not modify the WrappedComponent, so there are no side effects to worry about and component reuse is possible.

Pass through the unrelated props property to the WrappedComponent WrappedComponent

function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent name="name" {...this.props} />;
        }
    };
}
Copy the code

Don’t use higher-order components in the Render () method

class SomeComponent extends React.Component {
    renderConst EnchancedComponent = enhance(WrappedComponent); () {const EnchancedComponent = enhance(WrappedComponent); // Reloading a component causes the state of the original component and all its children to be lostreturn<EnchancedComponent />; }}Copy the code

Compose higher-order components with Compose

// Don't use const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)); // Lodash, Redux, Ramda, etc., all provide a function similar to 'compose', const enhance = compose(withRouter, The connect (commentSelector)); Const EnhancedComponent = enhance(WrappedComponent);Copy the code

Because a higher-order component implemented by convention is a pure function, if multiple functions have the same arguments (in this case, the functions returned by withRouter and connect require WrappedComponent arguments), So you can compose these functions using the compose method.

Using compose as a combination of higher-order components can significantly improve code readability and logical clarity.

The wrapper shows the name for easy debugging

Container components created by higher-order components behave as normal components in React Developer Tools. To facilitate debugging, you can select a display name that conveys the result of a higher-order component.

const getDisplayName = WrappedComponent => WrappedComponent.displayName || WrappedComponent.name || 'Component';
function HigherOrderComponent(WrappedComponent) {
    class HigherOrderComponent extends React.Component {/* ... */}
    HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(WrappedComponent)}) `;return HigherOrderComponent;
}
Copy the code

In fact, the Recompose library implements a similar feature, so you don’t have to write it yourself:

import getDisplayName from 'recompose/getDisplayName';
HigherOrderComponent.displayName = `HigherOrderComponent(${getDisplayName(BaseComponent)}) `; // Or, even better: import wrapDisplayName from'recompose/wrapDisplayName';
HigherOrderComponent.displayName = wrapDisplayName(BaseComponent, 'HigherOrderComponent');
Copy the code

Application scenarios of higher-order components

Technology that doesn’t talk about scenarios is playing the devil’s game, so here’s how to use higher-order components in business scenarios.

Access control

The conditional rendering feature of higher-order components can be used to control page permissions. Permissions are generally divided into two dimensions: page level and page element level. Here is an example of page level:

// HOC.js
function withAdminAuth(WrappedComponent) {
    return class extends React.Component {
    state = {
    isAdmin: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                isAdmin: currentRole === 'Admin'}); }render() {
           if (this.state.isAdmin) {
                return<WrappedComponent {... this.props} />; }else {
                return<div> You do not have permission to view this page, please contact the administrator! </div>); }}}; }Copy the code

Then there are two pages:

// pages/page-a.js
class PageA extends React.Component {
    constructor(props) {
        super(props);
        // something here...
    }
    componentWillMount() {
        // fetching data
    }
    render() {
        // render page with data

    }
}

export default withAdminAuth(PageA);
// pages/page-b.js
class PageB extends React.Component {
    constructor(props) {
        super(props);
        // something here...
    }

    componentWillMount() {
        // fetching data
    }
    render() {
        // render page with data

    }

}

export default withAdminAuth(PageB);
Copy the code

After using higher-order components to reuse the code, it can be very convenient to expand, for example, the product manager said that the PageC page should also have Admin permission to enter, We just need to nest the returned PageC withAdminAuth high-level component in pages/page-c.js, like withAdminAuth(PageC). Isn’t it perfect? Very efficient!! But.. The next day the product manager said that PageC page can be accessed as long as the VIP permission. You implement a higher-order component withVIPAuth by three strokes and five by two. Day three…

Instead of implementing the various withXXXAuth higher-order components, which themselves are highly similar in code, what we need to do is implement a function that returns higher-order components, removing the changed parts (Admin and VIP). Retain the unchanged part, the specific implementation is as follows:

// HOC.js
const withAuth = role => WrappedComponent => {
    return class extends React.Component {
        state = {
            permission: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                permission: currentRole === role,
            });
        }

        render() {
            if (this.state.permission) {
                return<WrappedComponent {... this.props} />; }else {
                return<div> You do not have permission to view this page, please contact the administrator! </div>); }}}; }Copy the code

After another layer of abstraction for higher-level components, withAdminAuth can now be written withAuth(‘ Admin ‘). If VIP privileges are required, simply pass ‘VIP’ into the withAuth function.

Notice how the react-Redux connect method is used? Yes, connect is also a function that returns higher-order components.

Component rendering performance tracking

The rendering time of a component can be easily recorded by capturing the life cycle of a child component with the parent component’s child component’s life cycle rule:

class Home extends React.Component {
    render() {
        return(<h1>Hello World.</h1>); }}function withTiming(WrappedComponent) {

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

WithTiming is a high-level component implemented using reverse inheritance that calculates the rendering time of the wrapped component (in this case, the Home component).

Page reuse

Suppose we have two pages, pageA and pageB, that render a list of movies in two categories, which might normally be written like this:

// pages/page-a.js
class PageA extends React.Component {
    state = {
        movies: [],
    }
    // ...

    async componentWillMount() {
        const movies = await fetchMoviesByType('science-fiction');
        this.setState({
            movies,
        });
    }

    render() {
        return <MovieList movies={this.state.movies} />
    }
}
export default PageA;

// pages/page-b.js
class PageB extends React.Component {
    state = {
        movies: [],
    }

    // ...
    async componentWillMount() {
        const movies = await fetchMoviesByType('action');
        this.setState({
            movies,

        });
    }
    render() {
        return <MovieList movies={this.state.movies} />
    }
}

export default PageB;
Copy the code

This may not be a problem when there are fewer pages, but if, as the business progresses, more and more types of movies need to be online, there will be a lot of repetitive code, so we need to refactor it:

const withFetching = fetching => WrappedComponent => {
    return class extends React.Component {
        state = {
            data: [],
        }
        async componentWillMount() {
            const data = await fetching();
            this.setState({
                data,
            });
        }
        render() {
        return<WrappedComponent data={this.state.data} {... this.props} />; } } } // pages/page-a.jsexport default withFetching(fetching('science-fiction'))(MovieList);

// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);

// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);
Copy the code

You will see that last withAuth function is similar to last withAuth, which draws the variable away from the external fetching to prevent reuse of the page.

Decorator mode? Higher order components? AOP?

As you may have already noticed, higher-order components are the implementation of the decorator pattern in React: By passing a component (function or class) to a function, enhancing that component (function or class) within the function (without modifying the parameters passed in), and finally returning that component (function or class), allowing new functionality to be added to an existing component without modifying the component, Belongs to a Wrapper Pattern.

Decorator mode: Dynamically adds additional properties or behaviors to an object while the program is running without changing the object itself.

The decorator pattern is a lighter and more flexible approach than using inheritance.

Implementing AOP using the Decorator pattern:

Aspect-oriented programming (AOP), like object-oriented programming (OOP), is just a programming paradigm that does not dictate how to implement AOP.

/ / perform before need to perform the Function of a Function. The Function of the newly added Function prototype. Before =function(before = () => {}) {
    return () => {
        before.apply(this, arguments);
        returnthis.apply(this, arguments); }; } // Execute a newly added Function Function function.prototype. after =function(after = () => {}) {
    return () => {
        const result = after.apply(this, arguments);
        this.apply(this, arguments);
        return result;
    };
}
Copy the code

You can see that before and after are actually higher-order functions, very similar to higher-order components.

Aspect oriented programming (AOP) is mainly used in functions unrelated to the core business but used in multiple modules, such as permission control, logging, data verification, exception handling, statistical reporting and so on.

Analogies to AOP should give you an idea of the type of problems that higher-order components typically deal with.

conclusion

High-order components in React are actually a very simple concept, but very practical. Rational use of higher-order components in actual business scenarios can improve code reusability and flexibility.

Finally, a little summary of higher-order components:

  • A higher-order component is not a component, but a function that converts one component into another
  • The main purpose of higher-order components is code reuse
  • The higher-order component is the implementation of the decorator pattern in React

Connect to higher-order components in React and their application scenarios