1. What is it#
A higher-order function (Higher-order function) is a function that satisfies at least one of the following conditions:
- Accepts one or more functions as input
- Outputs a function
In React
, a higher-order component accepts one or more components as parameters and returns a component. In essence, it is a function, not a component.
const EnhancedComponent = highOrderComponent(WrappedComponent);
In the above code, the function accepts a component WrappedComponent
as a parameter and returns a processed new component EnhancedComponent
.
This implementation of higher-order components is essentially a decorator design pattern.
2. How to write#
The basic template for writing higher-order components is as follows:
import React, { Component } from 'react';
export default (WrappedComponent) => {
return class EnhancedComponent extends Component {
// do something
render() {
return <WrappedComponent />;
}
}
}
By performing some operations on the incoming original component WrappedComponent
(such as manipulating props, extracting state, wrapping other elements around the original component, etc.), the desired component EnhancedComponent
is created.
Put common logic in higher-order components to achieve consistent processing of components and code reuse.
Therefore, the main function of higher-order components is to encapsulate and separate the common logic of components, so that the common logic can be better reused between components.
However, when using higher-order components, some conventions are generally followed, as follows:
- Keep props consistent
- You cannot use the ref attribute on functional (stateless) components because they do not have instances
- Do not change the original component WrappedComponent in any way
- Pass unrelated props attributes to the wrapped component WrappedComponent
- Do not use higher-order components in the render() method
- Use compose to combine higher-order components
- Wrap display names for easy debugging
It should be noted here that higher-order components can pass all props, but not refs.
If you want to add a ref reference to a higher-order component, the ref points to the instance of the outermost container component, not the wrapped component. If you need to pass refs, use React.forwardRef
, as follows:
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 forwardedRef to ref
return <WrappedComponent {...rest} ref={forwardedRef} />;
}
};
// The React.forwardRef method passes props and ref parameters to its callback function
// So the ref here is provided by React.forwardRef
function forwardRef(props, ref) {
return <Enhance {...props} forwardRef={ref} />
}
return React.forwardRef(forwardRef);
}
const EnhancedComponent = withLogging(SomeComponent);
3. Application scenarios#
From the above understanding, higher-order components can improve code reusability and flexibility. In practical applications, they are often used for functions that are unrelated to core business but are used in multiple modules, such as permission control, logging, data validation, exception handling, and statistics reporting.
For example, if there is a component that needs to retrieve data from the cache and then render it. In general, we would write it like this:
import React, { Component } from 'react'
class MyComponent extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
return <div>{this.state.data}</div>
}
}
The above code can certainly achieve this functionality, but if there are other components that have similar functionality, each component needs to repeat the code in componentWillMount
, which is obviously redundant.
The following can be rewritten using higher-order components, as follows:
import React, { Component } from 'react'
function withPersistentData(WrappedComponent) {
return class extends Component {
componentWillMount() {
let data = localStorage.getItem('data');
this.setState({data});
}
render() {
// Use {...this.props} to pass the attributes passed to the current component to the wrapped component WrappedComponent
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
class MyComponent2 extends Component {
render() {
return <div>{this.props.data}</div>
}
}
const MyComponentWithPersistentData = withPersistentData(MyComponent2)
Another example is component rendering performance monitoring, as follows:
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(`Rendering time of component ${WrappedComponent.name} is ${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}
export default withTiming(Home);