High-level component HOC
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.
Specifically, a higher-order component is a function that takes a component as an argument and returns a new component.
grammar
const EnhancedComponent = higherOrderComponent(WrappedComponent);
Copy the code
Usually we write contrast components, so what is a contrast component? A component converts the props property to UI, and a higher-order component converts one component to another.
Application scenarios
Higher-order components are common in React third-party libraries, such as Redux’s Connect method and Relay’s createContainer.
What is the meaning
Previously, mixins were used to address crosscutting concerns. But mixins create more problems than value. So remove mixins and find out more about how to convert components that you already use mixins. Obviously, crosscutting concerns are addressed using HOC.
The sample
1. Suppose there is a CommentList component that subscribs to and renders data from an external data source
// CommentList.js
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map(comment => (
<Comment comment={comment} key={comment.id} />
))}
</div>); }}Copy the code
2. Then, there is a component that subscribes to individual blog posts (BlogPost)
// BlogPost.js
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
}
}
conponentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
})
}
render() {
return <TextBlock text={this.state.blogPost} />; }}Copy the code
3. Above, the comments component CommentList differs from the article subscription component BlogPost in the following ways
- call
DataSource
In different ways - Render output is different
However, they have something in common
- When mounting a component to
DataSource
Add a listener that changes; - Within the listener, when the data source changes, it is called
setState
; - Remove change listeners when uninstalling components.
In a large application, the pattern of subscribing to data from a DataSource and calling setState will be used many times, when the front end will sniff out the code to tidy up, be able to extract the same place as an abstraction, and then many components can share it, which is the background for higher-order components.
4. We use a function withSubscription to allow it to do the following
const CommentListWithSubscription = withSubscription(
CommentList,
DataSource => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
Copy the code
The first argument to the withSubscription function above is the two components we wrote earlier, and the second argument retrieves the required data (DataSource and props).
What about this withSubscription function?
const withSubscription = (TargetComponent, selectData) = > {
return class extends React.Component {
constructor(props){
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount(){
DataSource.addChangeListener(this.handleChange);
}
componentDidMount(){
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render(){
return <TargetComponent data={this.state.data} {. this.props} / >}}}Copy the code
5. Under the summary
- A higher-order component neither modifies the parameter component nor copies its behavior using inheritance, and is simply a pure function with no side effects.
- The wrapped component receives all of the container
props
Properties and new datadata
For rendering output. Higher-order components don’t care about how the data is used, and wrapped components don’t care about where the data comes from. - The contract for higher order components and wrapped components is
props
Properties. This is where you can replace another higher-order component as long as they provide the sameprops
Attribute to the wrapped component. You can think of higher-level components as a set of thematic skins.
Instead of changing the original component, use composition
Now, we have a preliminary understanding of higher-order components, but in the actual business, when we write higher-order components, it is easy to write and modify the content of components, must resist the temptation. Such as
const logProps = (WrappedComponent) = > {
WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log('CurrentProps'.this.props);
console.log('NextProps', nextProps);
}
return WrappedComponent;
}
const EnhancedComponent = logProps(WrappedComponent);
Copy the code
Higher order componentslogProps
There are a couple of questions
- Wrapped component
WrappedComponent
Cannot be reused independently of enhanced components. - If you are in the
EnhancedComponent
Apply another higher-order component tologProps2
And will change as wellcomponentWillReceiveProps
, high order componentslogProps
“Will be overwritten. - Such higher-order components are not effective against functional components that have no life cycle
Aim at above problem, want to achieve same effect, can use combination way
const logProps = (WrappedComponent) = > {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: '.this.props);
console.log('Next props: ', nextProps);
}
render() {
// Wrap the input component in a container, do not modify it, nice!
return <WrappedComponent {. this.props} / >; }}}Copy the code
lenovo
If you haven’t noticed, high-level components and container component patterns have something in common.
- Container components focus on passing as part of a strategy for separating responsibilities between the upper and lower levels
props
Attribute to child components; - Higher-order components, which use containers as part of their implementation, are understood to be parameterized container component definitions.
Convention: Pass unrelated props properties to wrapped components throughout
The higher-order component returns a component that has a similar interface to the wrapped component.
render(){
// Filter out the props property specific to this higher-order component, and discard the extraProps
const{ extraProps, ... restProps } =this.props;
Inject injectedProps properties into the wrapped component. These are generally state values or instance methods
const injectedProps = {
// someStateOrInstanceMethod
};
return (
<WrappedComponent injectedProps={injectedProps} {. restProps} / >)}Copy the code
Conventions help ensure maximum flexibility and reusability for higher-order components.
Convention: Maximized combinativity
Not all higher-order components look the same. Sometimes they only receive a single argument, the wrapped component:
const NavbarWithRouter = withRouter(Navbar);
Copy the code
In general, higher-order components receive additional parameters. In the following example from Relay, a config object is used to specify the component’s data dependencies:
const CommentWithRelay = Relay.createContainer(Comment, config);
Copy the code
The most common signatures of higher-order components are as follows:
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
Copy the code
One way to think about it
// connect returns a function (higher-order component)
const enhanced = connect(commentSelector, commentActions);
const ConnectedComment = enhanced(Comment);
Copy the code
In other words, connect is a higher-order function that returns higher-order components! This form is somewhat confusing, but it has the property that a high-order function with only one argument (returned by connect) returns Component => Component, which allows functions of the same input and output types to be grouped together, together, together
/ / the pattern
const EnhancedComponent = withRouter(connect(commentSelector, commentActions)(Comment));
// Correct mode
// You can use a function composition tool
// compose(f, g, h) and (... args) => f(g(h(... Args))) is the same
const enhanced = compose(
withRouter,
connect(commentSelector, commentActions)
);
const EnhancedComponent = enhanced(Comment);
Copy the code
Many third-party libraries, including LoDash (e.g. Lodash.flowright), Redux, and Ramda, provide functions similar to compose’s functionality.
Convention: The wrapper displays the name for easy debugging
If your high-order component name is withSubscription and the display name of the wrapped component is CommentList, then the display name is withSubscription:
const withSubscription = (WrappedComponent) = > {
// return class extends React.Component { /* ... */ };
class WithSubscription extends React.Component { / *... * / };
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)}) `
return WithSubscription;
}
const getDisplayName = (WrappedComponent) = > {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
Copy the code
Here are a few don ‘ts
Do not use higher-order components within the Render method
**React’s difference algorithm (called coordination) ** uses component identifiers to determine whether to update an existing subtree or throw it away and remount a new one. If the render method returns exactly the same component as the previous render returned (===), React updates the subtree recursively by differentiating it from the new one. If they are not equal, the previous subtree is completely unloaded.
In general, you don’t have to think about the difference algorithm. But it has to do with higher order functions. Because it means you can’t apply higher-order functions to a component within its render method:
render() {
// Each render creates a new EnhancedComponent version
// EnhancedComponent1 ! == EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// causes the subobject tree to be completely unloaded/reloaded each time
return <EnhancedComponent />;
}
Copy the code
The above code can cause problems
- Performance issues
- Reloading a component causes the state of the original component and all of its children to be lost
Static methods must be copied
Problem: When you apply a higher-order component to a component, however, the original component is wrapped in a container component, which 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 = enhance(WrappedComponent);
// Enhanced components have no static methods
typeof EnhancedComponent.staticMethod === 'undefined' // true
Copy the code
Solution: (1) You can copy the methods of the original component to the container
function enhance(WrappedComponent) {
class Enhance extends React.Component {/ *... * /}
// you must know the method to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
Copy the code
(2) To do so, you need to know exactly what static methods you need to copy. You can use the hoist non-react statics for auto-handling, which automatically copies all non-React static methods:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/ *... * /}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
Copy the code
(3) Another possible solution is to export the component’s own static methods separately.
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
/ /... export the method separately...
export { someFunction };
/ /... and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
Copy the code
refs
Attributes cannot be passed through
In general, higher-order components can pass all props properties to wrapped components, but not refs references. Because refs is not a pseudo-property like key, React makes it special. If you add ref to the element of a component created by a higher-order component, ref points to the outermost container component instance, not the wrapped component.
React16 provides the React. ForwardRef API to address this issue, as described in the Refs transfer section.
You can also
React source code
React source code parsing overview