The Higher Order Component (HOC) is an advanced technique used in React for reusing component logic. HOC itself is not part of the React API, but rather a design pattern based on the compositional nature of React.
The name "Higher Order Component" itself exudes an air of sophistication, and in fact, this concept should be derived from higher-order functions in JavaScript. A higher-order function is a function that takes a function as input or returns a function as output. Currying can be thought of as a kind of higher-order function. Similarly, the React documentation also provides a definition of higher-order components, which are functions that accept a component and return a new component.
Specifically, a higher-order component is a function whose parameter is a component and whose return value is a new component. While a component transforms props
into UI, a higher-order component transforms a component into another component. HOCs are common in third-party libraries for React, such as the connect
function in Redux and the createFragmentContainer
function in Relay.
It's important to note that you should not attempt to modify the component prototype in any way within the HOC. Instead, you should use a compositional approach by wrapping the component in a container component to implement functionality. Generally, there are two typical ways to implement a higher-order component:
For example, we can add a id
property value from a store to the incoming component using a higher-order component. This allows us to add new props to the component. Of course, we can also manipulate the props
in the WrappedComponent
component in JSX, but it's important to note that we shouldn't directly modify the passed-in WrappedComponent
class. Instead, we can manipulate it in the process of composition.
We can also use a higher-order component to inject the state of the new component into the wrapped component. For example, we can use a higher-order component to convert an uncontrolled component into a controlled component.
Perhaps our goal is to wrap it with other components to achieve layout or style purposes.
Reverse inheritance means that the returned component inherits from the previous component. In reverse inheritance, we can do a lot of operations, such as modifying state
, props
, or even flipping the Element Tree
. The important point of reverse inheritance is that it cannot guarantee that the entire subtree of child components is resolved. This means that if the resolved element tree contains components (function type or Class
type), we cannot manipulate the child components of the components.
When we use reverse inheritance to implement higher-order components, we can control rendering through rendering interception. Specifically, we can consciously control the rendering process of WrappedComponent
, thereby controlling the result of rendering control. 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.
Since it is actually an inheritance relationship, we can read the component's props
and state
, and if necessary, even add, modify, or delete props
and state
. Of course, you need to control the risks brought by the modifications yourself. In some cases, we may need to pass some parameters to the higher-order attribute, and we can do so by passing parameters in a curried form, combined with higher-order components to achieve similar closure-like operations on components.
Both Mixin
and HOC
can be used to address cross-cutting concerns.
Mixin
is a mixed-in pattern. In practical use, the role of Mixin
is still very powerful. It allows us to share the same methods among multiple components, but it also continuously adds new methods and properties to the components. The component itself not only perceives it but even needs to deal with it (such as naming conflicts, state maintenance, etc.). When the number of mixed-in modules increases, the entire component becomes difficult to maintain. Mixin
may introduce invisible properties, such as using Mixin
methods in rendering components, which introduces invisible properties props
and state state
to the component. Additionally, Mixin
may depend on each other, couple with each other, and is not conducive to code maintenance. In addition, methods in different Mixin
s may conflict with each other. Previously, the React
official recommendation was to use Mixin
to address cross-cutting concerns, but because using Mixin
may cause more trouble, the official now recommends using HOC
.
Higher-order component HOC
belongs to the functional programming thought. The wrapped component does not perceive the existence of the higher-order component, and the component returned by the higher-order component has enhanced functionality above the original component. Based on this, React
officially recommends using higher-order components.
Do not attempt to modify the component prototype in the HOC
, or change it in any other way.
This can lead to some undesirable consequences; one is that the input component can no longer be used like it was before being enhanced with an HOC. Even worse, if you use another HOC that also modifies componentDidUpdate, the previous HOC will become ineffective, and this HOC will also be unable to be applied to function components without lifecycles.
Modifying the passed-in component in an HOC is a poor abstraction, as callers must know how they are implemented to avoid conflicts with other HOCs. HOCs should not modify the passed-in component but should use composition by implementing functionality through wrapping the component in a container component.
HOCs add features to the component and should not significantly alter the convention. 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 alike; sometimes it only accepts one parameter, which is the wrapped component.
HOCs can often accept multiple parameters, such as in Relay
, where HOC also receives a configuration object to specify the component's data dependency.
The most common HOC signature is as follows. connect
is a higher-order function that returns a higher-order component.
This form may seem confusing or unnecessary, but it has a useful property. Single-argument HOCs returned by functions have a signature of Component => Component
, making it easy to combine functions with output types similar to input types. This property also allows connect
and other HOCs to take on the role of decorators. Furthermore, many third-party libraries provide a compose
utility function, including lodash
, Redux
, and Ramda
.
React
's diff
algorithm uses component identifiers to determine whether to update an existing subtree or discard it and mount a new subtree. If the component returned from render
is the same ===
as the one in the previous render, React
recursively updates the subtree by distinguishing between the old and the new one. If they are not equal, it completely unmounts the previous subtree. Normally you don't have to worry about this, but for HOCs, this is very important because it means that you shouldn't apply an HOC to a component within the component's render
method.
This is not just a performance issue, remounting a component will cause the component and all of its children to lose their state. If the HOC is created outside of the component, then the component is only created once. So every time it's rendered, it's the same component. Normally, this is what you'd expect. In a few cases when you need to, you can dynamically call an HOC within the lifecycle methods of the component or in its constructor.
Sometimes defining static methods on a React
component can be useful, for example, a Relay
container exposes a static method getFragment
to ease composing GraphQL
fragments. But when you apply an HOC to a component, the original component is wrapped by the container component, which means the new component doesn't have any of the original component's static methods.
To solve this issue, you can copy these methods onto the container component before returning it.
To do this, you need to know which methods should be copied, and you can use the hoist-non-react-statics
dependency to automatically copy all non-React
static methods.
Another viable solution, besides exporting the component, is to separately export the static methods.
Although the convention with 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
, it's handled specifically by React
. If a ref is added to the returned component of an HOC, the ref will refer to the container component instead of the wrapped component. This issue can be addressed by explicitly forwarding the refs to the inner component using the React.forwardRef
API.