React high-order components and application scenarios

Posted by JADASDesigner on Thu, 12 Sep 2019 03:49:51 +0200

Links to the original text: https://juejin.im/post/5d7893d56fb9a06af92bcdbe

What are higher-order components?

Before explaining what higher-order components are, you can first understand what higher-order functions are, because their concepts are very similar. Here is the definition of higher-order functions:

If a function accepts one or more functions as parameters or returns a function, it can be called a higher-order function.

Here is a simple higher-order function:

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

The definition of higher-order components is very similar to that of higher-order functions.

If a function accepts one or more components as parameters and returns one component, it can be called a higher-order component.

Here's a simple high-order component:

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

So you may find that when a component returned from a higher-order component is a Stateless Component, the higher-order component is actually a higher-order function, because the stateless component itself is a pure function.

Stateless components are also called functional components.

High-order components in React

There are two main forms of higher-order components in React: attribute proxy and reverse inheritance.

Props Proxy

The simplest property broker implementation:

// Stateless
function HigherOrderComponent(WrappedComponent) {
    return props => <WrappedComponent {...props} />;
}
// or
// Stateful
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;

        }

    };
}
//Copy code

It can be found that the property broker is actually a function that accepts a WrappedComponent component as an input parameter and returns a class that inherits the React.Component component and returns the incoming WrappedComponent component in its render() method.

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

Because the higher-order component of the attribute broker type returns a standard React.Component component, what can be done in the React standard component, and what can be done in the higher-order component of the attribute broker type, such as:

  • Operation props
  • Detached state
  • Accessing component instances through ref
  • WrappedComponent, an incoming component, is wrapped with other elements

Operation props

Add new attributes for WrappedComponent:

function HigherOrderComponent(WrappedComponent) {

    return class extends React.Component {

        render() {

            const newProps = {

                name: 'love is a bitch',
                age: 18,
            };
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    };
}
//Copy code

Detached state

Use props and callback functions to extract state:

function withOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                name: '',
            };
        }

        onChange = () => {
            this.setState({
                name: 'love is a bitch',
            });
        }

        render() {
            const newProps = {
                name: {
                    value: this.state.name,
                    onChange: this.onChange,
                },
            };
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    };

}
//Copy code

How to use:

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

This converts input into a controlled component.

Accessing component instances through ref

Sometimes the ref attribute of a component is used when it is necessary to access a DOM element (using a third-party DOM operational library). It can only be declared on components of Class type, but not on components of function (stateless) type.

The value of ref can be either a string (not recommended) or a callback function. If it is a callback function, its execution time is:

  • After the component is mounted, the callback function is executed immediately, and the parameters of the callback function are instances of the component.
  • When a component is uninstalled or the original ref attribute itself changes, the callback function will be executed immediately, and the parameter of the callback function is null.

How do I get an instance of a WrappedComponent component in a higher-order component? The answer is that you can use the ref attribute of the WrappedComponent component, which executes the callback function of the ref at the time of the component component component DidMount 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 code

Note: ref properties cannot be used on stateless components (function type components), because stateless components have no instances.

WrappedComponent, an incoming component, is wrapped with other elements

Give the WrappedComponent package a div element with a background color of # fafafa fafa:

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

Inheritance Inversion

The simplest reverse inheritance implementation:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}
//Copy code

Reverse inheritance is actually a function that accepts a WrappedComponent component as a parameter and returns a class that inherits the WrappedComponent component and returns a super.render() method in the render() method of that class.

It will be found that there are some similarities between the implementation of attribute proxy and reverse inheritance, both returning a subclass inherited from a parent class, except that React.Component is inherited in attribute proxy and WrappedComponent is inherited in reverse inheritance.

What can reverse inheritance do?

  • Operation state
  • Render Highjacking

Operation state

The state in the WrappedComponent component instance can be read, edited, and deleted in the higher-order component. You can even add more state items, but it's 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 code

In this example, the WrappedComponent component is nested with additional elements by using the features of state and props that can be read in higher-order functions, and the States and props of WrappedComponent components are printed out.

Render Highjacking

Rendering hijacking is called because high-order components control the rendering output of WrappedComponent components. By rendering hijacking, we can:

  • Conditionally display the element tree
  • Operating on the React element tree output by render()
  • Operate props in any React element output by render()
  • WrappedComponent (same attribute proxy) wraps incoming components with other elements

conditional rendering

The condition props.isLoading is used to determine which component to render.

function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}
//Copy code

Operating on 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);
            return newTree;
        }
    };
}
//Copy code

Problems of High-order Components

  • Loss of static methods
  • refs attribute cannot be passed through
  • Reverse inheritance does not guarantee that a complete subcomponent tree is parsed

Loss of static methods

Because the original component is wrapped in a container component, it means that the new component will not have any static method of the original component:

// Define static methods
WrappedComponent.staticMethod = function() {}

// Using higher-order components

const EnhancedComponent = HigherOrderComponent(WrappedComponent);

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

So static methods must be copied:

function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    // You have to know how to copy it.
    Enhance.staticMethod = WrappedComponent.staticMethod;
    return Enhance;
}
//Copy code

One disadvantage of this is that you have to know what method to copy, but the React community implements a library hoist-non-react-statistics for automatic processing, which 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 code

refs attribute cannot be passed through

Generally speaking, higher-order components can pass all props to the wrapped component WrappedComponent, but there is one attribute that cannot be passed, which is ref. Unlike other attributes, React handles them specially.

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

If there is a need to pass ref, don't worry. React provides us with an API called React. forward Ref to solve this problem (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 forward Ref to ref
            return <WrappedComponent {...rest} ref={forwardedRef} />;
        }
    };
    // The React.forwardRef method passes in two parameters, props and ref, to its callback function.

    // So the ref here is provided by React. forward Ref.
    function forwardRef(props, ref) {
        return <Enhance {...props} forwardRef={ref} />
    }
    return React.forwardRef(forwardRef);
}

const EnhancedComponent = withLogging(SomeComponent);
//Copy code

Reverse inheritance does not guarantee that a complete subcomponent tree is parsed

React components come in two forms: class type and function type (stateless components).

We know that reverse inheritance rendering hijacking can control the rendering process of WrappedComponent, that is, we can do various operations on the results of elements tree, state, props or render().

But if the rendering elements tree contains a function-type component, then you can't manipulate the component's subcomponents.

High-order component conventions

While high-order components bring us great convenience, we should also follow some conventions:

  • props are consistent
  • You cannot use ref attributes on functional (stateless) components because it has no instances
  • Do not change the original component WrappedComponent in any way
  • Pass through the unrelated props attribute to the wrapped component WrappedComponent
  • Stop using higher-order components in render() methods
  • Combining higher-order components with compose
  • Packing display name for debugging

props are consistent

While adding features to subcomponents, higher-order components should try to keep the props of the original components as unaffected as possible, that is to say, the incoming components and the returned components are as consistent as possible on the props.

You cannot use ref attributes on functional (stateless) components because it has no instances

Functional components do not have instances themselves, so ref attributes do not exist.

Do not change the original component WrappedComponent in any way

Don't modify the prototype of a component in any way within a higher-level component. Think about 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 code

It will be found that the WrappedComponent has been modified inside the higher-order component. Once the original component has been modified, it loses the meaning of component reuse. So please return the new component through 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 parameter, do not modify it
            return <WrappedComponent {...this.props} />;
        }
    };
}
//Copy code

In this way, the optimized withLogging is a pure function and does not modify the WrappedComponent component, so there is no need to worry about any side effects to achieve component reuse.

Pass through the unrelated props attribute to the wrapped component WrappedComponent

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

Stop using higher-order components in render() methods

class SomeComponent extends React.Component {
    render() {
        // Each time a higher-order function is called, a new component is returned.
        const EnchancedComponent = enhance(WrappedComponent);
        // Every time render, the sub-object tree is completely unloaded and re-loaded
        // Reloading a component causes the state of the original component and all its subcomponents to be lost
        return <EnchancedComponent />;
    }
}
//Copy code

Combining higher-order components with compose

// Don't use it like that.
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// These higher-order components can be combined using a compose function
// lodash, redux, ramda and other third-party libraries provide functions similar to `compose'.
const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);
//Copy code

Because a high-order component implemented by convention is actually a pure function, if the parameters of multiple functions are the same (in this case, the parameters of functions returned by withRouter function and commentSelector are WrappedComponent), then these functions can be combined by compose method.

Using compose to combine higher-order components can significantly improve code readability and logical clarity.

Packing display name for debugging

Container components created by higher-order components perform the same in React Developer Tools as other common components. To facilitate debugging, you can choose a display name to convey 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 code

In fact, recompose library implements a similar function, lazy words can not write their own:

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

Application scenarios for higher-order components

Skills without scenarios are hooligans, so let's talk about how to use high-level components in business scenarios.

Authority control

Using the conditional rendering feature of high-order components, the page can be controlled by permission. The permission control is generally divided into two dimensions: page level and page element level. Here, a chestnut is listed at 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 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 code

After using high-level components to reuse the code, it is very convenient to expand. For example, the product manager said that PageC pages also need Admin permission to access. We only need to nest the returned PageC in pages/page-c.js with a layer of high-level components of withAdminAuth, just like with AdminAuth (PageC). Is it perfect? Very efficient!! But. The next day the product manager said that the PageC page could be accessed with VIP permission. You implement a high-order component withVIPAuth by dividing three into five. On the third day...

In fact, you can also be more efficient, that is, to abstract a layer above the higher-order components, without implementing various high-order components with XXXAuth, because the high-order components themselves are highly similar in code, so what we need to do is to implement a function that returns to the higher-order components and extracts the variable parts (Admin, VIP). To retain the unchanged part, the concrete realization 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 code

It can be found that after a layer of abstraction of high-level components, the previous withAdminAuth can be written as withAuth('Admin'). If you need VIP permission at this time, you just need to pass in'VIP'in the withAuth function.

Have you found that the connection method of react-redux is very similar to that of react-redux? Yes, connect is actually a function that returns higher-order components.

Component Rendering Performance Tracking

By capturing the life cycle of a child component with the lifecycle rules of parent component and child component, it is convenient to record the rendering time of a component:

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() {
            return super.render();
        }
    };
}
export default withTiming(Home);
//Copy code

WithTime is a high-order component implemented by reverse inheritance. Its function is to calculate the rendering time of the wrapped component (Home component in this case).

Page reuse

Suppose we have two pages, pageA and pageB, rendering the list of movies in two categories, the common way of writing might be as follows:

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

There may be no problem when there are fewer pages, but if more and more types of movies need to be online as business progresses, there will be a lot of duplicate code written, so we need to reconstruct:

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.js
export 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 code

You will find that withFetching is actually similar to the previous withAuth function, which extracts the fetching(type) from the outside and passes it in, thus realizing page reuse.

Decorator mode? High-order components? AOP?

Maybe you have found that higher-order components are actually the implementation of the decorator pattern in React: by passing a function into a component (function or class), the function of the component (function or class) is enhanced within the function (without modifying the input parameters), and finally the component (function or class) is returned. That is to say, it allows adding new functions to an existing component without modifying it. It belongs to Wrapper Pattern.

What is the Decorator pattern: add some additional attributes or behaviors to the object dynamically during the running of the program without changing the object itself.

Decorator mode is a lighter and more flexible approach than using inheritance.

Implementing AOP with Decorator Mode:

Aspect-oriented programming (AOP), like object-oriented programming (OOP), is only a programming paradigm and does not specify how to implement AOP.

// Execute a newly added function before the function that needs to be executed
Function.prototype.before = function(before = () => {}) {
    return () => {
        before.apply(this, arguments);
        return this.apply(this, arguments);

    };
}
// Execute a newly added function after the function that needs to be executed
Function.prototype.after = function(after = () => {}) {
    return () => {
        const result = after.apply(this, arguments);
        this.apply(this, arguments);
        return result;
    };
}
//Copy code

It can be found that before and after are high-order functions, which are very similar to high-order components.

Aspect-Oriented Programming (AOP) is mainly used in the fields of authority control, log recording, data verification, exception handling, statistical reporting and so on, which are independent of core business but are used in many modules.

By analogy with AOP, you should know what type of problems higher-order components usually deal with.

summary

The higher-order component in React is actually a very simple concept, but it is also very practical. Reasonable use of high-level components in real business scenarios can improve code reusability and flexibility.

Finally, a brief summary of high-order components is given.

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

Reference connection: High-order components in React and their application scenarios


Author: Leng Xing 1024
Link: https://juejin.im/post/5d7893d56fb9a06af92bcdbe
Source: Nuggets
Copyright belongs to the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

Topics: React Attribute Programming JSON