Turn:
Reuse of React components
Reuse of React components
At present, the engineering of the front end is becoming more and more important. Although Ctrl+C and Ctrl+V can also meet the requirements, it is a huge task once facing modification. Therefore, it is particularly important to reduce the copy of code, increase the packaging and reuse ability, and realize maintainable and reusable code. In React, components are the main unit of code reuse, The component reuse mechanism based on composition is quite elegant, but it is not so easy to reuse more fine-grained logic (state logic, behavior logic, etc.), and it is difficult to disassemble the state logic as a reusable function or component. In fact, before the emergence of Hooks, there was a lack of a simple and direct component behavior extension method, for Mixin, HOC Render Props is an upper layer mode explored under the existing (component mechanism) game rules. It has not solved the problem of logical reuse between components from the root until Hooks stepped on the stage. Let's introduce the reuse methods among Mixin, HOC, Render Props and Hooks.
Mixin
Of course, React did not recommend using Mixin as a reuse solution a long time ago, but it can still support Mixin through create React class. In addition, note that Mixin is not supported when declaring components in the class mode of ES6.
Mixins allows multiple React components to share code. They are very similar to mixins in Python or traits in PHP. The emergence of mixin scheme comes from an OOP intuition and only provides React in the early stage Createclass () API (React v15.5.0 is officially abandoned and moved to create React class) to define components. Naturally, (class) inheritance has become an intuitive attempt. Under the JavaScript prototype based extension mode, mixin similar to inheritance has become a good solution. Mixin is mainly used to solve the reuse of life cycle logic and state logic, Allowing external extension of component life cycle is particularly important in Flux and other modes, but there are many defects in continuous practice:
- There are implicit dependencies between components and Mixin (Mixin often relies on specific methods of components, but it is not known when defining components).
- Conflicts may occur between multiple mixins (for example, the same state field is defined).
- Mixin tends to add more states, which reduces the predictability of applications and leads to a sharp increase in complexity.
- Implicit dependency leads to the opacity of dependency relationship, and the maintenance cost and understanding cost rise rapidly.
- It is difficult to quickly understand the component behavior. We need to fully understand all the extension behaviors that rely on Mixin and their interactions
- The method and state fields of the component itself are not easy to delete because it is difficult to determine whether Mixin depends on it.
- Mixin is also difficult to maintain, because mixin logic will eventually be leveled and merged, so it is difficult to figure out the input and output of a mixin.
There is no doubt that these problems are fatal, so reactv0 13.0 abandons Mixin static crosscutting (similar to inherited reuse) and moves to HOC high-order components (similar to combined reuse).
Examples
In the example of the ancient version, a common scenario is: a component needs to be updated regularly. It is easy to use setInterval(), but it is very important to cancel the timer to save memory when it is not needed. React provides a life cycle method to inform the time of component creation or destruction. In the following Mixin, use setInterval() and ensure that the timer is cleared when the component is destroyed.
var SetIntervalMixin = { componentWillMount: function() { this.intervals = []; }, setInterval: function() { this.intervals.push(setInterval.apply(null, arguments)); }, componentWillUnmount: function() { this.intervals.forEach(clearInterval); } }; var TickTock = React.createClass({ mixins: [SetIntervalMixin], // Reference mixin getInitialState: function() { return {seconds: 0}; }, componentDidMount: function() { this.setInterval(this.tick, 1000); // Method to call mixin }, tick: function() { this.setState({seconds: this.state.seconds + 1}); }, render: function() { return (React has been running for {this.state.seconds} seconds.
); } }); ReactDOM.render( , document.getElementById("example") );
HOC
In fact, when we think of high-order function as the name of high-order function, we can get the name of high-order function from the concept of high-order function. In fact, when we think of high-order function as the function of high-order function, we can accept the function of high-order function, Similarly, the definition of high-order components is also given in the React document. High-order components are functions that receive components and return new components. The specific meaning is: the high-order component can be regarded as an implementation of the decoration mode by React. The high-order component is a function that accepts a component as a parameter and returns a new component. It will return an enhanced React component. The high-order component can make our code more reusable, logical and abstract, and hijack the render method, You can also control props and state.
Comparing mixin and HOC, mixin is a mixed mode. In actual use, mixin is very powerful. It can make us share the same method in multiple components, but it will also add new methods and attributes to the components. The components themselves can not only be aware, but also need to do relevant processing (such as naming conflict, state maintenance, etc.), Once there are more mixed modules, the whole component becomes difficult to maintain. Mixin may introduce invisible attributes. For example, using mixin method in rendering components will bring invisible attributes props and state to the component. Moreover, mixin may be interdependent and coupled, which is not conducive to code maintenance. In addition, the methods in different mixins may conflict with each other. Previously, React officials suggested using mixin to solve problems related to crosscutting concerns, but since using mixin may cause more trouble, the official now recommends using HOC. High order component HOC belongs to the idea of functional programming. For wrapped components, the existence of high-order components will not be perceived, and the components returned by high-order components will have the effect of function enhancement on the original components. Based on this, React officially recommends the use of high-order components.
Although HOC does not have so many fatal problems, it also has some small defects:
- Extensibility limitation: HOC cannot completely replace Mixin. In some scenarios, Mixin can and HOC cannot, such as PureRenderMixin, because HOC cannot access the State of sub components from the outside and filter out unnecessary updates through shouldComponentUpdate. Therefore, React provides React after supporting ES6Class Purecomponent to solve this problem.
- Ref delivery problem: ref is cut off. The delivery problem of ref is quite annoying under layers of packaging. The function ref can alleviate part of the problem (let the HOC know the creation and destruction of nodes), so that there is react later forwardRef API.
- Wrapperhell: the proliferation of HOC leads to wrapperhell (there is no problem that can not be solved by one layer of package, if there is, there are two layers). Multi-layer abstraction also increases the complexity and understanding cost, which is the most critical defect, but there is no good solution in the hoc mode.
Examples
Specifically, a high-level component is a function whose parameter is a component and its return value is a new component. A component converts props into UI, while a high-level component converts a component into another component. HOC s are common in third-party libraries of React, such as Redux's connect and Relay's createFragmentContainer.
// High order component definition const higherOrderComponent = (WrappedComponent) => { return class EnhancedComponent extends React.Component { // ... render() { return ; } }; } // Common component definition class WrappedComponent extends React.Component{ render(){ //.... } } // Returns the enhancement component wrapped by the higher-order component const EnhancedComponent = higherOrderComponent(WrappedComponent);
It should be noted here that do not try to modify the component prototype in HOC in any way, but use the combination method to realize the function by wrapping the component in the container component. Generally, there are two ways to implement high-order components:
- Property proxy.
- Reverse Inheritance Inversion.
Attribute agent
For example, we can add a stored id attribute value to the incoming component. Through high-level components, we can add a props for this component. Of course, we can also operate props in the WrappedComponent component in JSX. Note that we should not directly modify the incoming component instead of operating the incoming WrappedComponent class, It can be operated in the process of combination.
const HOC = (WrappedComponent, store) => { return class EnhancedComponent extends React.Component { render() { const newProps = { id: store.id } return ; } } }
We can also use high-order components to load the state of new components into packaged components. For example, we can use high-order components to convert uncontrolled components into controlled components.
class WrappedComponent extends React.Component { render() { return ; } } const HOC = (WrappedComponent) => { return class EnhancedComponent extends React.Component { constructor(props) { super(props); this.state = { name: "" }; } render() { const newProps = { value: this.state.name, onChange: e => this.setState({name: e.target.value}), } return ; } } }
Or our goal is to wrap it with other components to achieve the purpose of layout or style.
const HOC = (WrappedComponent) => { return class EnhancedComponent extends React.Component { render() { return ( ); } } }
Reverse inheritance
Reverse inheritance means that the returned component inherits the previous component. In reverse inheritance, we can do a lot of operations, such as modifying state, props, or even flipping Element Tree. Reverse inheritance has an important point. Reverse inheritance cannot ensure that the complete sub component tree is resolved, that is, the resolved Element Tree contains components (function type or Class type), You can no longer operate on the sub components of the component.
When we use reverse inheritance to implement high-order components, we can control rendering through rendering hijacking, specifically, we can consciously control the rendering process of WrappedComponent, so as to control the result of rendering control. For example, we can decide whether to render components according to some parameters.
const HOC = (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { render() { return this.props.isRender && super.render(); } } }
We can even hijack the life cycle of the original component by rewriting.
const HOC = (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { componentDidMount(){ // ... } render() { return super.render(); } } }
Because it is actually an inheritance relationship, we can read the props and state of the component. If necessary, we can even modify, add, modify and delete props and state. Of course, the premise is that the risks caused by modification need to be controlled by yourself. In some cases, we may need to pass in some parameters for high-order attributes, so we can pass in parameters in the form of curry, and cooperate with high-order components to complete closure like operations on components.
const HOCFactoryFactory = (params) => { // Operate params here return (WrappedComponent) => { return class EnhancedComponent extends WrappedComponent { render() { return params.isRender && this.props.isRender && super.render(); } } } }
be careful
Do not change the original components
Do not attempt to modify the component prototype in HOC or otherwise change it.
function logProps(InputComponent) { InputComponent.prototype.componentDidUpdate = function(prevProps) { console.log("Current props: ", this.props); console.log("Previous props: ", prevProps); }; // Returns the original input component, which has been modified. return InputComponent; } // Each time logProps is called, the enhanced component will have log output. const EnhancedComponent = logProps(InputComponent);
This will have some adverse consequences. First, the input component can no longer be used as before the HOC enhancement. More seriously, if you use another HOC that will also modify componentDidUpdate to enhance it, the previous HOC will fail, and this HOC cannot be applied to function components without life cycle.
Modifying the HOC of the incoming component is a bad abstraction, and the caller must know how they are implemented to avoid conflicts with other HOC. HOC should not modify the incoming components, but should use the combination method to realize the function by wrapping the components in the container components.
function logProps(WrappedComponent) { return class extends React.Component { componentDidUpdate(prevProps) { console.log("Current props: ", this.props); console.log("Previous props: ", prevProps); } render() { // Wrap the input component in a container without modifying it, Nice! return ; } } }
Filter props
HOC adds features to components and should not significantly change the agreement. The components returned by HOC should maintain similar interfaces with the original components. The HOC should transparently transmit props that have nothing to do with itself. Most HOCs should include a render method similar to the following.
render() { // Filter out additional props and do not transmit through const { extraProp, ...passThroughProps } = this.props; // Inject props into the packaged component. // Usually the value of state or instance method. const injectedProp = someStateOrInstanceMethod; // Pass props to packaged components return ( ); }
Maximize composability
Not all HOC are the same. Sometimes it only accepts one parameter, that is, the wrapped component.
const NavbarWithRouter = withRouter(Navbar);
The HOC can usually receive multiple parameters. For example, in the Relay, the HOC receives an additional configuration object to specify the data dependency of the component.
const CommentWithRelay = Relay.createContainer(Comment, config);
The most common HOC signature is as follows. connect is a high-order function that returns high-order components.
// 'connect' function of React Redux const ConnectedComment = connect(commentSelector, commentActions)(CommentList); // connect is a function whose return value is another function. const enhance = connect(commentListSelector, commentListActions); // The return value is HOC, which will return the components connected to the Redux store const ConnectedComment = enhance(CommentList);
This form may seem confusing or unnecessary, but it has a useful attribute. For example, the single parameter HOC returned by the connect function has a signature component = > component. Functions with the same output type and input type can be easily combined. The same attributes allow connect and other HOC to assume the role of decorator. In addition, many third-party libraries provide compose tool functions, including lodash, Redux and Ramda.
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // You can write composite tool functions // compose(f, g, h) is equivalent to (... Args) = > F (g (H (... Args))) const enhance = compose( // These are single parameter HOC withRouter, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)
Do not use HOC in the render method
The diff algorithm of React uses the component ID to determine whether it should update the existing subtree or discard it and mount a new subtree. If the component returned from render is the same as the component in the previous rendering = = =, React recursively updates the subtree by distinguishing the subtree from the new subtree. If they are not equal, the previous subtree will be completely unloaded.
Usually you don't need to consider this when using it, but this is very important for HOC, because it means that you shouldn't apply HOC to a component in the render method of the component.
render() { // Each call to the render function creates a new EnhancedComponent // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // This will cause the subtree to be unloaded and remounted every time it is rendered! return ; }
This is not just a performance problem. Reloading a component will cause the state of the component and all its sub components to be lost. If you create a HOC outside the component, the component will only be created once. Therefore, each render will be the same component. Generally speaking, this is consistent with your expected performance. In rare cases, you need to call HOC dynamically. You can call it in the component's life cycle method or its constructor.
Be sure to copy static methods
Sometimes it is useful to define static methods on React components. For example, the Relay container exposes a static method getFragment to facilitate the combination of GraphQL fragments. However, when you apply HOC to a component, the original component will be wrapped with a container component, which means that the new component does not have any static methods of the original component.
// Define static functions WrappedComponent.staticMethod = function() {/*...*/} // Now use HOC const EnhancedComponent = enhance(WrappedComponent); // The enhancement component does not have a staticMethod typeof EnhancedComponent.staticMethod === "undefined" // true
To solve this problem, you can copy these methods to the container component before returning.
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // You must know exactly which methods should be copied:( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
But to do this, you need to know which methods should be copied. You can use hoist non React statistics to rely on automatic copying of 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; }
In addition to exporting components, another feasible solution is to export this static method additionally.
// Use this method instead of MyComponent.someFunction = someFunction; export default MyComponent; // ... Export this method separately export { someFunction }; // ... And in the component to be used, import them import MyComponent, { someFunction } from "./MyComponent.js";
Refs will not be passed
Although the Convention of high-level components is to pass all props to the packaged components, this is not applicable to refs. That is because ref is not actually a prop. Just like key, it is specially processed by React. If you add ref to the return component of HOC, the ref reference points to the container component instead of the wrapped component. This problem can be solved through React Forwardref API explicitly forwards refs to internal components..
function logProps(Component) { class LogProps extends React.Component { componentDidUpdate(prevProps) { console.log('old props:', prevProps); console.log('new props:', this.props); } render() { const {forwardedRef, ...rest} = this.props; // Define the custom prop attribute "forwardedRef" as ref return ; } } // Pay attention to react The second parameter of the forwardref callback is "ref". // We can pass it to LogProps as a regular prop attribute, such as "forwardedRef" // Then it can be mounted on the sub components wrapped by LogProps. return React.forwardRef((props, ref) => { return ; }); }
Render Props
Like HOC, render props is also a veteran mode that has always existed. Render props refers to a simple technology that uses a props with a value of function to share code between React components. The component with render props receives a function, which returns a React element and calls it instead of implementing its own rendering logic, Render props is a function used to tell the component what to render. Props is also an implementation of component logical reuse. In short, in the reused component, through a prop attribute named render (the attribute name can also be not render, as long as the value is a function), this attribute is a function, This function accepts an object and returns a subcomponent. The object in this function parameter will be passed to the newly generated component as props. When using the caller component, you only need to decide where to render this component and what logic to render and pass in the relevant object.
Comparing HOC and Render Props, technically, both are based on the component composition mechanism. Render Props has the same extension ability as HOC. It is called Render Props. It does not mean that it can only be used to reuse rendering logic, but that components are combined through render() in this mode, which is similar to the combination relationship established through Wrapper's render() in HOC mode, The two are very similar. They will also produce a layer of Wrapper. In fact, Render Props and HOC can even convert to each other.
Similarly, there are some problems with Render Props:
- The data flow direction is more intuitive, and the descendant components can clearly see the data source, but in essence, Render Props is implemented based on closures. A large number of reuse of components will inevitably introduce the problem of callback shell.
- The context of the component is missing, so there is no this Props attribute. You can't access this like HOC props. children.
Examples
React