Keywords: high-order function, high-order component, property broker, reverse inheritance, decorator pattern, controlled component

Contents of this article:

  • What are higher-order components
  • Higher-order components in React
    • Properties Proxy (Props Proxy)
    • Inheritance Inversion
  • Problems with higher-order components
  • Conventions for higher-order components
  • Application scenarios of higher-order components
  • Decorator mode? Higher order components? AOP?
  • conclusion

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 stateless
function HigherOrderComponent(WrappedComponent) {
    return props= ><WrappedComponent {... props} />; }// or
/ / state
function 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:

  • operationprops
  • Pull awaystate
  • throughrefAccess the component instance
  • Wrap the incoming component with other elementsWrappedComponent

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 =(a)= > {
            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:

  • After the component is mounted (componentDidMount), the callback function executes immediately and takes an instance of the component.
  • Component uninstalled (componentDidUnmount) or the originalrefIf the property itself changes, the callback function will be executed immediately, and the parameter of the callback function isnull.

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: Cannot be used on stateless components (function-type components)refProperty because stateless components have no instances.

Wrap the incoming component with other elementsWrappedComponent

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() {
            return super.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:

  • operationstate
  • 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 the state and props of the WrappedComponent.

Rendering hijacked

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

  • Conditionally present the element tree (element tree)
  • Operation byrender()The React element tree is output
  • In anyrender()The React element in the outputprops
  • Wrap the incoming component with other elementsWrappedComponent(withThe property broker)
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 { return super.render(); }}}; }Copy the code
Modify the React element tree output by render()

Modify element tree:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            const tree = super.render();
            const newProps = {};
            if (tree && tree.type === 'input') {
                newProps.value = 'something here';
            }
            constprops = { ... tree.props, ... newProps, };const newTree = React.cloneElement(tree, props, tree.props.children);
            returnnewTree; }}; }Copy the code

Problems with higher-order components

  • Static method loss
  • refsAttribute cannot be passed through
  • 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 static methods
WrappedComponent.staticMethod = function() {}
// Use higher-order components
const EnhancedComponent = HigherOrderComponent(WrappedComponent);
// Enhanced components have no static methods
typeof EnhancedComponent.staticMethod === 'undefined' // true
Copy the code

So we must copy the static method:

function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    // You must know the method to copy
    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 ref
            return<WrappedComponent {... rest} ref={forwardedRef} />; }}; // the callback function of the forwardRef(props,) function is the function of the forwardRef(props,) function 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:

  • propsconsistent
  • You cannot use it on functional (stateless) componentsrefProperty because it has no instance
  • Do not change the original component in any wayWrappedComponent
  • Transparent transmission is irrelevantpropsAttribute to the wrapped componentWrappedComponent
  • Don’t berender()Method uses higher-order components
  • usecomposeCompose higher-order components
  • 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.

Do not change the original WrappedComponent

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 the parameter through without modifying it
            return <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

Do not use higher-order components in the Render () method

class SomeComponent extends React.Component {
    render() {
        // A new component is returned each time a higher-order function is called
        const EnchancedComponent = enhance(WrappedComponent);
        // Each time render is performed, the subobject tree is completely unmounted and restarted
        // Reloading a component causes the state of the original component and all its children to be lost
        return <EnchancedComponent />; }}Copy the code

Compose higher-order components with Compose

// Don't use it this way
constEnhancedComponent = withRouter (connect (commentSelector) (WrappedComponent));// You can compose these higher-order components using a compose function
// Lodash, Redux, Ramda and other third-party libraries provide functions similar to 'compose'
constEnhance = compose(withRouter, connect(commentSelector));constEnhancedComponent = 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 needed, just 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 isThe ${this.end - this.start} ms`);
        }
        render() {
            return super.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.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 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.

// Execute a newly added function function before the function that needs to be executed
Function.prototype.before = function(before = () = >{{})return (a)= > {
        before.apply(this.arguments);
        return this.apply(this.arguments);
    };
}
// Execute a newly added function function after the function that needs to be executed
Function.prototype.after = function(after = () = >{{})return (a)= > {
        this.apply(this.arguments);
        return after.apply(this.arguments);
    };
}
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