How To Reuse React Components

Mixins, HOC, render props, and Hooks are four ways to reuse components

Sabesan Sathananthan
Published in
12 min readJun 24, 2021

--

Photo by Vlada Karpovich from Pexels

Now frontend engineering is more and more important. Although Ctrl+C and Ctrl+V can also be used to complete requirements, once they are modified, it becomes a huge task. Therefore, copying of code is reduced, and the packaging and reuse capabilities are increased to achieve maintainability and reversibility. The code used becomes particularly important.

In React, components are the main unit of code reuse. The combination-based component reuse mechanism is quite elegant, but for more fine-grained logic (state logic, behavior logic, etc.), reuse is not so easy. It is difficult to disassemble the state logic as a reusable function or component. In fact, before the appearance of Hooks, there was a lack of a simple and direct way of component behavior extension, which is considered to be mixins, higher-order components (HOC), and render props. The upper-level model explored under the existing (component mechanism) game rules has not solved the problem of logic reuse between components from the root. This is my 38th Medium article.

Mixins

Of course, React no longer recommends using mixins as a reuse solution for a long time, but it can still provide support for mixins through create-react-class. Note that mixins are not supported when declaring components in ES6 classes.

Mixins allow multiple React components to share code. They are very similar to mixins in Python or traits in PHP. The emergence of the mixin solution comes from an OOP intuition. In the early days, it only provided React.createClass() API to define components. (In React v15.5.0, it is officially abandoned and moved to create-react-class). Naturally, (class) inheritance has become an intuitive attempt, and in JavaScript prototype-based extension mode, it is similar to the inherited mixin scheme. It has become a good solution. Mixin is mainly used to solve the reuse problem of life cycle logic and state logic, and allows the component life cycle to be extended from the outside. This is especially important in Flux and other modes, but many defects have also appeared in continuous practice:

  • There is an implicit dependency between the component and the mixin (Mixin often depends on the specific method of the component, but the dependency is not known when the component is defined).
  • There may be conflicts between multiple mixin (such as defining the same state field).
  • Mixin tends to add more states, which reduces the predictability of the application and leads to a sharp increase in complexity.
  • Implicit dependencies lead to opaque dependencies, and maintenance costs and understanding costs are rising rapidly.
  • It is difficult to quickly understand the behavior of components, and it is necessary to fully understand all the extension behaviors that rely on mixin and their mutual influence.
  • The method and state field of the component itself is afraid to be easily deleted because it is difficult to determine whether mixin depends on it.
  • Mixin is also difficult to maintain, because Mixin logic will eventually be flattened and merged together, and 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 abandoned Mixin static crosscutting (similar to inherited reuse) and moved to HOC higher-order components (similar to combined reuse).

Example

The example of the ancient version, a common scenario is: A component needs to be updated regularly. It is easy to do it with setInterval(), but it is very important to cancel the timer when it is not needed to save memory. React provides a lifecycle method to inform the component. The time of creation or destruction, the following Mixin, use setInterval() and ensure that the timer is cleaned up when the component is destroyed.

HOC

After Mixin, HOC high-order components take on the heavy responsibility and become the recommended solution for logical reuse between components. High-order components reveal a high-order atmosphere from their names. In fact, this concept should be derived from high-order functions of JavaScript. The high-order function is a function that accepts a function as input or output. It can be thought that currying is a higher-order function. The definition of higher-order components is also given in the React document. Higher-order components receive components and return new components. function. The specific meaning is: High-order components can be seen as an implementation of React decoration pattern. High-order components are a function, and the function accepts a component as a parameter and returns a new component. It will return an enhanced React components. High-order components can make our code more reusable, logical and abstract, can hijack the render method, and can also control propsand state.

Comparing Mixin and HOC, Mixin is a mixed-in mode. In actual use, Mixin is still very powerful, allowing us to share the same method in multiple components, but it will also continue to add new methods and attributes to the components. The component itself can not only perceive but also need to do related processing (such as naming conflicts, state maintenance, etc.). Once the mixed modules increase, the entire component becomes difficult to maintain. Mixin may introduce invisible attributes, such as in the Mixin method used in the rendering component brings invisible property props and states to the component. Mixin may depend on each other and is coupled with each other, which is not conducive to code maintenance. In addition, the methods in different Mixin may conflict with each other. Previously React officially recommended using Mixin to solve problems related to cross-cutting concerns, but because using Mixin may cause more trouble, the official recommendation is now to use HOC. High-order component HOC belong to the idea of ​​ functional programming. The wrapped components will not be aware of the existence of high-order components, and the components returned by high-order components will have a functional enhancement effect 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 minor flaws:

  • Scalability restriction: HOC cannot completely replace Mixin. In some scenarios, Mixin can but HOC cannot. For example, PureRenderMixin, because HOC cannot access the State of subcomponents from the outside, and at the same time filter out unnecessary updates through shouldComponentUpdate. Therefore, React After supporting ES6Class, React.PureComponent is provided to solve this problem.
  • Ref transfer problem: Ref is cut off. The transfer problem of Ref is quite annoying under the layers of packaging. The function Ref can alleviate part of it (allowing HOC to learn about node creation and destruction), so the React.forwardRef API API was introduced later.
  • WrapperHell: HOC is flooded, and WrapperHell appears (there is no problem that cannot be solved by one layer, if there is, then two layers). Multi-layer abstraction also increases complexity and cost of understanding. This is the most critical defect. In HOC mode There is no good solution.

Example

Specifically, a high-order component is a function whose parameter is a component and the return value is a new component. A component converts props into a UI but a high-order component converts a component into another component. HOC is very common in React third-party libraries, such as Redux’s connect and Relay’s createFragmentContainer.

Attention should be paid here, do not try to modify the component prototype in the HOC in any way, but should use the combination method to realize the function by packaging the component in the container component. Under normal circumstances, there are two ways to implement high-order components:

  • Property agent Props Proxy.
  • Reverse inheritance Inheritance Inversion.

Property Agent

For example, we can add a stored id attribute value to the incoming component. We can add a props to this component through high-order components. Of course, we can also operate on the props in the WrappedComponent component in JSX. Note that it is not to manipulate the incoming WrappedComponent class, we should not directly modify the incoming component, but can operate on it in the process of combination.

We can also use high-order components to load the state of new components into the packaged components. For example, we can use high-order components to convert uncontrolled components into controlled components.

Or our purpose is to wrap it with other components to achieve the purpose of layout or style.

Reverse inheritance

Reverse inheritance means that the returned component inherits the previous component. In reverse inheritance, we can do a lot of operations, modify state, props and even flip the Element Tree. There is an important point in the reverse inheritance that reverse inheritance cannot ensure that the complete sub-component tree is parsed. That means if the parsed element tree contains components (function type or Class type), the sub-components of the component can no longer be manipulated.

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 to control the results of rendering control. For example, we can decide whether to render components according to some parameters.

We can even hijack the life cycle of the original component by rewriting.

Since it is actually an inheritance relationship, we can read the props and state of the component. If necessary, we can even add, modify, and delete the props and state. Of course, the premise is that the risks caused by the modification need to be controlled by yourself. In some cases, we may need to pass in some parameters for the high-order attributes, then we can pass in the parameters in the form of currying, and cooperate with the high-order components to complete the operation similar to the closure of the component.

note

Don’t change the original components

Don’t try to modify the component prototype in HOC, or change it in other ways.

Doing so will have some undesirable consequences. One is that the input component can no longer be used as before the HOC enhancement. What is more serious is that if you use another HOC that also modifies componentDidUpdate to enhance it, the previous HOC will be invalid, and this HOC cannot be applied to functional components that have no 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 a combination of components to achieve functions by packaging the components in container components.

Filter props

HOC adds features to components and should not significantly change the convention itself. The components returned by HOC should maintain similar interfaces with the original components. HOC should transparently transmit props that have nothing to do with itself, and most HOC should include a render method similar to the following.

Maximum composability

Not all HOCs are the same. Sometimes it only accepts one parameter, which is the packaged component.

const NavbarWithRouter = withRouter(Navbar);

HOC can usually receive multiple parameters. For example, in Relay, HOC additionally receives a configuration object to specify the data dependency of the component.

const CommentWithRelay = Relay.createContainer(Comment, config);

The most common HOC signatures are as follows, connect is a higher-order function that returns higher-order components.

This form may seem confusing or unnecessary, but it has a useful property, like the single-parameter HOC returned by the connect function has the signature Component => Component , and functions with the same output type and input type can be easily combined. The same attributes also allow connect and other HOCs to assume the role of decorator. In addition, many third-party libraries provide compose tool functions, including lodash, Redux, and Ramda.

Don’t use HOC in the render method

React ’s diff algorithm uses the component identifier to determine whether it should update the existing subtree or discard it and mount the new subtree. If the component returned from the render is the same as the component in the previous render ===, React passes The subtree is distinguished from the new subtree to recursively update the subtree, and if they are not equal, the previous subtree is completely unloaded.
Usually, you don’t need to consider this when using it, but it is very important for HOC, because it means that you should not apply HOC to a component in the render method of the component.

This is not just a performance issue. Re-mounting the component will cause the state of the component and all its subcomponents to be lost. If the HOC is created outside the component, the component will only be created once. So every time you render it 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 lifecycle 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 composition of GraphQL fragments. But when you apply HOC to a component, the original component will be packaged with a container component, which means that the new component does not have any static methods of the original component.

To solve this problem, you can copy these methods to the container component before returning.

But to do this, you need to know which methods should be copied. You can use hoist-non-react-statics to automatically copy all non-React static methods.

In addition to exporting components, another feasible solution is to additionally export this static method.

Refs will not be passed

Although the convention of high-level components is to pass all props to the packaged component, this does not apply to refs, because ref is not actually a prop, just like a key, it is specifically handled by React. If the ref is added to the return component of the HOC, the ref reference points to the container component, not the packaged component. This problem can be explicitly forwarded to the internal component through the React.forwardRefAPI refs.

Render Props

Like HOC, Render props is also a veteran model that has always existed. render props refers to a simple technology that uses a props valued as a function to share code between a kind of React components. A component with render props receives a function. This function Return a React element and call it instead of implementing its own rendering logic. Render props is a function props used to tell the component what content needs to be rendered. It is also a way to implement component logic reuse. Simply put, it is being copied. In the component used, pass a prop property named render (the property name may not render, as long as the value is a function). The property is a function. This function accepts an object and returns a subcomponent, which will The object in the function parameter is passed as props to the newly generated component, and when using the caller component, you only need to decide where to renderthe component and what logic to renderand pass in the relevant object.
Comparing HOC and Render props, technically, both are based on the component combination mechanism. Render props has the same extensibility as HOC. It is called Render props. It does not mean that it can only be used to reuse rendering logic, but that it is here. In this mode, the components are combined through render(), similar to the establishment of a combination relationship through Wrapper’s render() in HOC mode. The two are very similar, and they will also produce a layer of Wrapper. In fact, Render props and HOC . It can even be converted to each other.
Similarly, Render props will have some problems:

  • The data flow is more intuitive. The descendant components can clearly see the source of the data, but in essence, Render props is implemented based on closures. A large number of component reuse will inevitably introduce the callback hell problem.
  • The context of the component is lost, so there is no this.propsproperty, and this.props.childern cannot be accessed like HOC.

Hooks

There are endless code reuse solutions, but overall code reuse is still very complicated. A large part of this is because fine-grained code reuse should not be bundled with component reuse. HOC, Render props, etc. are based on component combination The solution is equivalent to first packaging the logic to be reused into components, and then using the component reuse mechanism to achieve logic reuse. Naturally, it is limited to component reuse, so there are problems such as limited scalability, Ref partition, Wrapper Hell, etc. , Then we need to have a simple and direct way of code reuse. Functions. Separating reusable logic into functions should be the most direct and cost-effective way of code reuse. But for state logic, some abstract patterns are still needed. (Such as Observable) can be reused, which is exactly the idea of ​​Hooks, using functions as the smallest code reuse unit, and built-in some modes to simplify the reuse of state logic. Compared with the other solutions mentioned above, Hooks makes the logic reuse within the component no longer bundled with the component reuse. It is really trying to solve the problem of fine-grained logic reuse (between components) from the lower level. In addition, this statement The modular logic reuse scheme further extends the explicit data flow and combination ideas between components to the components.
File Hooks are not perfect either, but for now, its disadvantages are as follows:

  • The additional learning cost mainly lies in the comparison between Functional Component and Class Component .
  • There are restrictions on the writing method (cannot appear in conditions, loops), and the writing restrictions increase the cost of reconstruction.
  • It destroys the performance optimization effect of PureComponent and React.memo shallow comparison. In order to get the latest props and state, the event function must be recreated every render()
  • In closure scenarios, old state and props values may be referenced.
  • The internal implementation is not intuitive, relying on a mutable global state, and no longer so pure.
  • React.memo can’t completely replace shouldComponentUpdate (because state change is not available, only for props change).
  • The useStateAPI is not perfect in design.

--

--

Sabesan Sathananthan
Codezillas

Software Engineer 👨‍💻 @SyscoLABSSL | Postgard🧑‍🎓 in CSE at UOM | Technical Writer ✍️ | sabesansathananthan.now.sh | Still makes silly mistakes daily.