As frontend engineering becomes increasingly important, while using Ctrl+C and Ctrl+V can meet the requirements, it becomes a massive task when it comes to making modifications. Therefore, reducing code duplication and increasing encapsulation for reusability become particularly crucial. In React, components are the primary units for code reuse, and the component reuse mechanism based on composition is quite elegant. However, reusing more granular logic (such as state logic and behavior logic) is not that easy. It's difficult to extract 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 way to extend component behavior. Mixin, HOC, and Render Props are considered higher-level patterns explored within the existing (component mechanism) game rules and have not effectively addressed the problem of logic reuse between components until the emergence of Hooks. Now, let's introduce the four ways of component reuse: Mixin, HOC, Render Props, and Hooks.
Of course, React has long ceased to recommend the use of Mixin as a solution for reuse. However, it is still possible to support Mixin through create-react-class. Additionally, when declaring components using the class approach in ES6, Mixin is not supported.
Mixins allow multiple React components to share code, similar to mixins in Python or traits in PHP. The emergence of the Mixin solution stems from an object-oriented programming intuition. Back in the early days, the React.createClass() API was introduced (officially deprecated in React v15.5.0 and moved to create-react-class) to define components, and naturally, (class) inheritance became an intuitive attempt. Under the prototype-based extension pattern in JavaScript, the Mixin solution, similar to inheritance, became a good solution. Mixin is mainly used to solve the reuse problem of lifecycle logic and state logic, allowing the extension of component lifecycle from the outside, especially important in patterns like Flux. However, in practice, many defects have emerged:
Mixin (Mixin often depends on specific methods of the component, but these dependency relationships are not known when defining the component).Mixin (e.g., defining the same state fields).Mixin tends to add more states, reducing application predictability and significantly increasing complexity.Mixin and their mutual influence.state fields are not easily modified due to the difficulty of determining whether Mixin depends on them.Mixin is also challenging to maintain because the logic of Mixin will eventually be flattened and merged together, making it difficult to understand the input and output of a Mixin.Undoubtedly, these issues are fatal. Therefore, React v0.13.0 abandoned the static cross-cutting of Mixin (similar to inheritance for reuse) and shifted to HOC (High Order Component) for reuse, which resembles composition.
In the ancient version, a common scenario is that a component needs regular updates. It's easy to do with setInterval(), but it's crucial to clear the timer when it's no longer needed to save memory. React provides lifecycle methods to inform when a component should be created or destroyed. The following Mixin uses setInterval() and ensures the timer is cleared when the component is destroyed.
After Mixin, HOC a high-order component takes on the responsibility and becomes the recommended solution for logical reuse between components. The name "high-order component" reveals its advanced nature. In reality, this concept is derived from the high-order function in JavaScript. A high-order function is a function that takes a function as input or output. Currying is a type of high-order function. Similarly, the React documentation also provides the definition of high-order components, which are functions that receive components and return new components. Specifically, HOC can be regarded as an implementation of the decorator pattern by React. A 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. High-order components can make our code more reusable, logical, and abstract, and can intercept the render method, as well as control props and state, among other things.
In comparison to Mixin and HOC, Mixin is a pattern of mixing. In actual use, the role of Mixin is still very powerful, enabling us to share the same methods among multiple components. However, it also continuously adds new methods and properties to the components. Components not only can perceive this, but may even need to do related processing (such as name conflicts, status maintenance, etc.). Once the number of mixed modules increases, the entire component becomes difficult to maintain. Mixin may introduce invisible properties, such as using Mixin methods in rendering components, which brings invisible attributes props and state state to the component. Additionally, Mixin may have mutual dependencies, mutual couplings, and is not conducive to code maintenance. Furthermore, methods from different Mixins may conflict with each other. Previously, the React official recommendation was to use Mixin to solve cross-cutting concerns, however, using Mixin may cause more trouble, so the official now recommends using HOC. High-order component HOC belongs to the functional programming concept. The component being wrapped will not be aware of the existence of the high-order component, and the component returned by the high-order component will have an enhanced effect on the original component. Based on this, React officially recommends using high-order components.
Although HOC does not have as many fatal problems, it also has some small flaws:
HOC cannot completely replace Mixin. In some scenarios, Mixin can do what HOC cannot, such as PureRenderMixin, because HOC cannot access the State of the child component from the outside, and filter out unnecessary updates through shouldComponentUpdate. Therefore, after supporting ES6Class, React introduced React.PureComponent to solve this problem.Ref passing problem: Refs are isolated, and the passing problem of Refs becomes quite annoying under multiple layers of wrapping. The function Ref can relieve some of these issues (letting HOC know about node creation and destruction), hence the later introduction of the React.forwardRef API.HOC leads to "Wrapper Hell" (there's no problem that can't be solved by adding another layer, and if there is, then add two layers). Multiple layers of abstraction also increase complexity and understanding cost, which is the most critical flaw, and there is no good solution under the HOC pattern.Specifically, a high-order component is a function that takes a component as a parameter and returns a new component. A component transforms props into UI, while a high-order component transforms a component into another component. HOC is very common in third-party libraries in React, such as Redux's connect and Relay's createFragmentContainer.
Here, it is important to note not to attempt to modify the component prototype in any way in the HOC, but rather to use a composition approach by implementing functionality through wrapping the component in a container component. Typically, there are two ways to implement a high-order component:
For example, we can add a storage id attribute value to the incoming component. Through a high-order component, we can add a new props to this component. Of course, we can also manipulate the props in the JSX of the WrappedComponent component. It is important to note that we should not directly modify the incoming component, but we can manipulate it in the process of composition.
We can also use a higher-order component to inject the state of a new component into the wrapped component. For instance, we can use a higher-order component to convert an uncontrolled component into a controlled component.
Alternatively, we may want to wrap it with other components to achieve layout or styling purposes.
Reverse inheritance means that the returned component inherits from the previous component. In reverse inheritance, we can perform many operations, such as modifying state, props, or even reversing the Element Tree. A crucial point about reverse inheritance is that it cannot guarantee the complete rendering of the child component tree, which means that once the rendered element tree contains components (function types or Class types), further manipulation of the child components is not possible.
When we use reverse inheritance to implement a higher-order component, we can control rendering through render interception, meaning that we can consciously control the rendering process of WrappedComponent to control the rendering result. For example, we can decide whether to render the component based on certain parameters.
We can even intercept the original component's lifecycle through overriding.
As it's essentially an inheritance relationship, we can access the component's props and state, and if necessary, even modify, add, or delete props and state, provided that you control the risks brought by the modifications. In some cases, when we may need to pass some parameters to the higher-order props, we can do so by currying the parameters, combined with higher-order components to achieve closure-like operations on the component.
Do not attempt to modify the component prototype within the HOC or alter it in any other way.
Doing this will have some adverse consequences; one being that the input component can no longer be used as it was before being enhanced by the HOC. More critically, if you use another HOC that also modifies componentDidUpdate, the previous HOC will become ineffective, and this HOC will also not be applicable to function components without lifecycles. Modifying the HOC of the input component is a poor abstraction that requires callers to know how they are implemented in order to avoid conflicts with other HOCs. HOCs should not modify the input components; instead, they should use a composition approach by implementing functionality through wrapping the component in a container component.
HOCs add features to components and should not drastically change conventions. The component returned by the HOC should maintain a similar interface to the original component. Most HOCs should include a render method similar to the one below.
Not all HOCs are the same; sometimes they only accept one parameter, which is the component being wrapped.
HOCs often can accept multiple parameters, for example, in Relay, the HOC additionally accepts a configuration object to specify the component's data dependencies.
The most common HOC signature is as follows. connect is a higher-order function that returns a higher-order component.
This form may look confusing or unnecessary, but it has a useful property. A single-parameter HOC, like the one returned by the 'connect' function, has a signature of Component => Component, making it easy to combine functions with the same input and output types. This property also allows 'connect' and other HOCs to act as decorators. Furthermore, many third-party libraries provide a 'compose' utility function, including 'lodash', 'Redux', and 'Ramda'.
React's diffing algorithm uses component identities to determine whether to update an existing subtree or discard it and mount a new one. If the component returned from 'render' is the same === as the one in the previous render, React recursively updates the subtree. If they are not the same, the previous subtree is completely unmounted. While this is generally not something that needs to be worried about, for HOCs this is crucial because it means that you should not apply an HOC to a component in its 'render' method.
This is not just a performance issue; remounting a component causes it and all its subcomponents to lose their state. If the HOC is created outside the component, it is created only once, so the same component is rendered every time, which is generally the expected behavior. In very rare cases where you need to call an HOC dynamically, it can be done in the component's lifecycle methods or its constructor.
Sometimes it's useful to define static methods on a React component, such as Relay containers exposing a static method 'getFragment' for composing GraphQL fragments. However, when you apply an HOC to a component, the original component is wrapped with the container component, meaning the new component does not have any of the original component's static methods.
To solve this, you can copy these methods onto the container component before returning it.
To simplify this, you can use the 'hoist-non-react-statics' dependency to automatically copy all non-React static methods.
In addition to exporting the component, another feasible solution is to export the static method separately.
While the convention for higher-order components is to pass all props to the wrapped component, this does not apply to refs because ref is not actually a prop, just like key, it is specifically handled by React. If a ref is added to the returned component of a HOC, the ref will refer to the container component rather than the wrapped component. This problem can be addressed by using the React.forwardRef API to explicitly forward refs to the inner component.
Like HOCs, Render Props have been an ancient pattern that has been around for a long time. Render Props refers to a simple technique of sharing code between React components using a function with a value as props. Components with render props receive a function that returns a React element and call it instead of implementing their own rendering logic. Render Props are a way of informing a component about what content needs to be rendered, and it is also a way of reusing component logic. Simply put, in a component that needs to be reused, a prop attribute (the property name does not have to be "render", as long as the value is a function) called render is used. This attribute is a function that accepts an object and returns a child component. It will pass the object in the function argument as props to the newly generated component, and in the context of the calling component, it only needs to decide where to render this component and how to logically render it and pass in the relevant object.
Compared to HOCs and Render Props, technically, both are based on component composition mechanisms. Render Props have the same extension capability as HOCs, which is called Render Props. This does not mean that it can only be used to reuse rendering logic, but it means that in this pattern, components are combined using render(). Similar to the relationship established by the render() method of Wrapper in the HOC pattern, the two are very similar in form, and both will create an additional layer of Wrapper. In fact, Render Props and HOCs can even be interconverted.
Similarly, Render Props also have some issues:
this.props property, and it cannot access this.props.children like HOCs.The solutions for code reuse are numerous, but overall code reuse is still quite complex. One of the main reasons for this lies in the fact that fine-grained code reuse should not be tied to component reuse. Solutions based on component composition such as HOC and Render Props first wrap the reusable logic into components, then use the component reuse mechanism to achieve logic reuse. Therefore, they are naturally limited by component reuse, leading to limited extensibility, Ref barriers, and "Wrapper Hell" issues. Therefore, we need a simple and direct way of code reuse, i.e., functions. Abstracting the reusable logic into functions should be the most direct and cost-effective way of code reuse. However, for state logic, some abstract patterns (such as Observable) are still needed to achieve reuse. This is exactly the idea behind Hooks – treating functions as the smallest unit of code reuse, while also incorporating some patterns to simplify the reuse of state logic. Compared to the other solutions mentioned above, Hooks decouple internal logic reuse from component reuse, which truly attempts to address the fine-grained logic reuse issue from the bottom layer (between components). Additionally, this declarative logic reuse approach further extends the explicit data flow between components and the composition idea to the inside of components.
Of course, Hooks are not perfect. However, for the time being, its drawbacks are as follows:
Functional Component and Class Component.PureComponent and React.memo shallow comparisons. To obtain the latest props and state, a new event handling function must be re-created for each render().state and props values.React.memo cannot completely replace shouldComponentUpdate (since it cannot detect state change, only for props change).useState API.