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 Mixin
s 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: Ref
s are isolated, and the passing problem of Ref
s 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
.