When any project grows to a certain complexity, it is bound to face the problem of logic reuse. In React, there are several ways to implement logic reuse: Mixin, HOC, Decorator, Render Props, Hook, etc. This article mainly analyzes the advantages and disadvantages of these ways to help developers make a more suitable way for business scenarios.

Mixin

Mixin has been widely used in various object-oriented languages to create a multi-inheritance-like effect for single-inheritance languages, and is perhaps the first approach that developers moving from Vue to React can think of. Although React has now abandoned it, Mixin was indeed a design pattern that React used to implement code sharing.

The mixin method, in the broad sense, is a way to assign the methods in the mixin object to the original object to achieve object mixing, similar to the role of Object.assign() in ES6. The principle is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const mixin = function (obj, mixins) {
  const newObj = obj
  newObj.prototype = Object.create(obj.prototype)

  for (let prop in mixins) {
    // Iterate through mixins' properties
    if (mixins.hasOwnPrototype(prop)) {
      // Determine if it is the mixin's own property
      newObj.prototype[prop] = mixins[prop]; // 
    }
  }
  return newObj
};

Using Mixin in React

Assuming that multiple components in our project need to set the default name property, using mixin allows us to not have to write multiple identical getDefaultProps methods in different components, we can define a mixin

1
2
3
4
5
6
7
const DefaultNameMixin = {
  getDefaultProps: function () {
    return {
      name: "Joy"
    }
  }
}

In order to use mixin, you need to add the mixins property to the component, and then wrap our written mixin into an array and use it as the value of the mixins property

1
2
3
4
5
6
const ComponentOne = React.createClass({
  mixins: [DefaultNameMixin]
  render: function () {
    return <h2>Hello {this.props.name}</h2>
  }
})

The written mixin can be reused in other components.

Since the mixins property value is an array, it means that we can call multiple mixins in the same component. A slight change in the above example gives us

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const DefaultFriendMixin = {
  getDefaultProps: function () {
    return {
      friend: "Yummy"
    }
  }
}

const ComponentOne = React.createClass({
  mixins: [DefaultNameMixin, DefaultFriendMixin]
  render: function () {
    return (
      <div>
        <h2>Hello {this.props.name}</h2>
        <h2>This is my friend {this.props.friend}</h2>
      </div>
    )
  }
})

We can even include other mixins within a mixin.

For example, write a new mixin DefaultProps containing the above DefaultNameMixin and DefaultFriendMixin

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const DefaultPropsMixin = {
  mixins: [DefaultNameMixin, DefaultFriendMixin]
}

const ComponentOne = React.createClass({
  mixins: [DefaultPropsMixin]
  render: function () {
    return (
      <div>
        <h2>Hello {this.props.name}</h2>
        <h2>This is my friend {this.props.friend}</h2>
      </div>
    )
  }
})

At this point, we can conclude that mixin has at least the following advantages:

  • The possibility of using the same mixin in multiple components.
  • The possibility of using multiple mixins in the same component.
  • Multiple mixins can be nested within the same mixin

However, in different scenarios, advantages can become disadvantages.

  • Breaking the encapsulation of the original component and possibly having to go and maintain new states such as state and props.
  • The naming in different mixins is unpredictable and very prone to conflicts
  • May create recursive call problems, increasing project complexity and maintenance difficulties

In addition, mixin has its own logic for handling issues such as state conflicts, method conflicts, and the calling order of multiple lifecycle methods. If you are interested, you can refer to the following articles:

Higher-Order Components(HOC)

Since mixin has the above-mentioned shortcomings, React stripped mixin and replaced it with HOC.

HOC is essentially a function that accepts a component as an argument and returns a new component.

React officially uses HOC when implementing some public components, such as withRouter in react-router, and connect in Redux. Here’s an example of withRouter.

By default, the component must be rendered by Route route match before this.props exists, before it has route parameters, and before it can execute this.props.history.push('/next') to jump to the page corresponding to the route using the functional navigation writeup. The role of withRouter in HOC is to wrap a component that is not wrapped by Route route into Route, so that the three objects history, location and match of react-router can be put into the props property of the component, thus enabling functional navigation jumping.

The principle of withRouter implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const withRouter = (Component) => {
  const displayName = `withRouter(${Component.displayName || Component.name})`
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(
            context,
            `You should not use <${displayName} /> outside a <Router>`
          );
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          )
        }}
      </RouterContext.Consumer>
    )
}

Use code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from "react"
import { withRouter } from "react-router"
class TopHeader extends Component {
  render() {
    return (
      <div>
        导航栏
        {/* Click to jump to login */}
        <button onClick={this.exit}>Exit</button>
      </div>
    )
  }

  exit = () => {
    // After the withRouter HOC wrapping, you can use this.props for jumping operations
    this.props.history.push("/login")
  }
}
// Use the withRouter wrapper component to return history, location, etc.
export default withRouter(TopHeader)

Since HOC is essentially a method that gets a component and returns a new component, it can theoretically implement multiple nesting just like mixin.

For example:

Write a HOC that empowers singing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'

const widthSinging = WrappedComponent => {
	return class HOC extends Component {
		constructor () {
			super(...arguments)
			this.singing = this.singing.bind(this)
		}

		singing = () => {
			console.log('i am singing!')
		}

		render() {
			return <WrappedComponent />
		}
	}
}

Write a HOC that empowers dancing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'

const widthDancing = WrappedComponent => {
	return class HOC extends Component {
		constructor () {
			super(...arguments)
			this.dancing = this.dancing.bind(this)
		}

		dancing = () => {
			console.log('i am dancing!')
		}

		render() {
			return <WrappedComponent />
		}
	}
}

Use the above HOC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import React, { Component } from "react"
import { widthSing, widthDancing } from "hocs"

class Joy extends Component {
  render() {
    return <div>Joy</div>
  }
}

// Empowering Joy's singing and dancing talents
export default widthSinging(withDancing(Joy))

As you can see above, with a simple wrap using HOC, you can turn the original simple Joy into a nightclub prince who can both sing and dance!

Conventions for using HOC

When using HOC, there are some inkling conventions:

  • Passing irrelevant Props to wrapper components (passing props that are not related to their specific content).
  • step-by-step combination (avoiding different forms of HOC calls in tandem).
  • include display displayName for easy debugging (each HOC should conform to a regular display name).
  • do not use higher-order components in the render function (each time render, the higher order returns a new component, affecting diff performance).
  • static methods must be copied (the new component returned after a higher order does not contain the static methods of the original component).
  • Avoid using ref (ref will not be passed);

Advantages and disadvantages of HOC

At this point we can summarize the advantages of the Higher Order Component (HOC):

  • HOC is a pure function, easy to use and maintain.
  • Again, since HOC is a pure function, it supports the passing of multiple parameters, enhancing its scope of application.
  • HOC returns a component, which can be combined and nested for flexibility.

Of course there are some problems with HOC.

  • When multiple HOCs are used in nesting, it is impossible to directly determine from which HOC the props of the child component is responsible for passing.
  • When parent and child components have the same name props, it leads to the problem that the parent component overwrites the child component with the same name props, and react does not report errors, which is low developer-awareness.
  • each HOC returns a new component, thus creating a lot of useless components, while deepening the component hierarchy and not making it easy to troubleshoot problems.

Decorator and HOC belong to the same schema and will not be discussed here.

Render Props

This is the official React definition of Render Props

The term “render prop” refers to a technique for sharing code between React components using a prop whose value is a function.

Official example:

1
<DataProvider render={(data) => <h1>Hello {data.target}</h1>} />

As above, the DataProvider component has a props property called render (it can also be called by other names), which is a function, and this function returns a React Element that is rendered inside the component by calling this function, then this component uses the render props technique.

Readers may wonder, “Why do we need to call the props property to achieve internal rendering of the component instead of rendering directly inside the component”? To borrow from the official React answer, render props is not a skill that every React developer needs to master, and you may never even use this method, but it does exist to provide developers with an additional option when thinking about component code sharing.

Render Props usage scenarios

We may need to use pop-up windows frequently in project development, pop-up windows UI can be varied, but the function is similar, that is, open and close. Take antd as an example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Modal, Button } from "antd"
class App extends React.Component {
  state = { visible: false }

  // Control pop-up windows to show and hide
  toggleModal = (visible) => {
    this.setState({ visible })
  };

  handleOk = (e) => {
    // Do something
    this.setState({ visible: false })
  }

  render() {
    const { visible } = this.state
    return (
      <div>
        <Button onClick={this.toggleModal.bind(this, true)}>Open</Button>
        <Modal
          title="Basic Modal"
          visible={visible}
          onOk={this.handleOk}
          onCancel={this.toggleModal.bind(this, false)}
        >
          <p>Some contents...</p>
        </Modal>
      </div>
    )
  }
}

The above is the simplest example of using Model, even if it is simple to use, we still need to pay attention to its display state and implement its switching method. But the developer really only wants to focus on the business logic related to onOk, the ideal usage should be like this

1
2
3
4
5
6
<MyModal>
  <Button>Open</Button>
  <Modal title="Basic Modal" onOk={this.handleOk}>
    <p>Some contents...</p>
  </Modal>
</MyModal>

The above usage can be achieved by render props:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { Modal, Button } from "antd"
class MyModal extends React.Component {
  state = { on: false }

  toggle = () => {
    this.setState({
      on: !this.state.on
    })
  }

  renderButton = (props) => <Button {...props} onClick={this.toggle} />

  renderModal = ({ onOK, ...rest }) => (
    <Modal
      {...rest}
      visible={this.state.on}
      onOk={() => {
        onOK && onOK()
        this.toggle()
      }}
      onCancel={this.toggle}
    />
  )

  render() {
    return this.props.children({
      Button: this.renderButton,
      Modal: this.renderModal
    })
  }
}

This completes a Modal with state and basic functionality, and we only need to focus on specific business logic when we use the Modal in other pages.

As you can see above, render props is a real React component, not just a function that returns a component like HOC, which also means that using render props will not create the problem of nested components like HOC, and you don’t have to worry about the override problem caused by props naming conflicts.

render props usage restrictions

The use of arrow functions in render props should be avoided because of the performance impact this can cause.

For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Bad example
class MouseTracker extends React.Component {
  render() {
    return (
      <Mouse render={mouse => (
        <Cat mouse={mouse} />
      )}/>
    )
  }
}

This is not a good way to write it, because the render method is possible to render multiple times, and using the arrow function will cause the value passed into render to be different each time it is rendered, while there is actually no difference, which will lead to performance problems.

So a better way to write it would be to define the function passed into render as an instance method, so that even though we render it multiple times, it is always the same function that is bound.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 好的示例
class MouseTracker extends React.Component {
  renderCat(mouse) {
  	return <Cat mouse={mouse} />
  }

  render() {
    return (
		  <Mouse render={this.renderTheCat} />
    )
  }
}

Pros and cons of render props

Advantages

  • props naming is modifiable and does not override each other.
  • Clarity of props origin.
  • No multi-layer nesting of components.

Disadvantages

  • Cumbersome writing style.
  • inability to access data outside of the return statement.
  • prone to function callback nesting.

The following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const MyComponent = () => {
  return (
    <Mouse>
      {({ x, y }) => (
        <Page>
          {({ x: pageX, y: pageY }) => (
            <Connection>
              {({ api }) => {
                // yikes
              }}
            </Connection>
          )}
        </Page>
      )}
    </Mouse>
  )
}

Hook

The core of React is components, so React has been working on optimizing and refining the way components are declared. From the earliest class components to function components, there are advantages and disadvantages to each. Class components provide us with a complete lifecycle and state, but they are clunky to write, while function components are very simple and lightweight, but they have the limitation that they must be pure functions, cannot contain state, and do not support lifecycle.

The React team felt that the best way to write components was as functions, not classes, which led to the creation of React Hooks.

React Hooks is designed to be an enhanced version of the function component, a full-featured component written without using “classes” at all.

Why class components are “bulky”, to borrow from the official React example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { Component } from "react"

export default class Button extends Component {
  constructor() {
    super()
    this.state = { buttonText: "Click me, please" }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    this.setState(() => {
      return { buttonText: "Thanks, been clicked!" }
    })
  }
  render() {
    const { buttonText } = this.state
    return <button onClick={this.handleClick}>{buttonText}</button>
  }
}

The above is a simple button component that contains the most basic state and click methods, and the state changes when the button is clicked.

This is a very simple functional component, but it requires a lot of code to implement. Since function components don’t contain state, we can’t use a function component to declare a component with the above functionality. But we can use Hook to implement it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import React, { useState } from "react"

export default function Button() {
  const [buttonText, setButtonText] = useState("Click me,   please")

  function handleClick() {
    return setButtonText("Thanks, been clicked!")
  }

  return <button onClick={handleClick}>{buttonText}</button>
}

In comparison, Hook appears to be more lightweight and preserves its own state while staying close to the function component.

In addition to the first hook useState() introduced in the above example, React also officially provides hooks useEffect(), useContext(), useReducer() and so on. See the official hooks and their usage for details.

The flexibility of Hook also lies in the fact that, in addition to the officially provided basic hooks, we can use these basic hooks to wrap and customize hooks for easier code reuse.

Hook Pros and Cons

Advantages

  • Easier to reuse code
  • Cleaner code style
  • Less code volume

Disadvantages

  • State is not synchronized (functions run independently and each function has a separate copy of the scope)
  • Need to use useEffect more rationally
  • Small granularity, need to abstract a lot of hook for complex logic

Summary

Except for Mixin, which is a little bit behind because of its obvious shortcomings, there is no best solution for HOC, render props, and react hook, which all have both advantages and disadvantages.Even for the most popular react hook, although each hook looks so short and refreshing, but in the actual business, usually a business function corresponds to multithe hook, which means that when the business changes, you need to maintain multiple hook changes, compared to maintaining a class, the mental burden may be This means that when the business changes, you need to maintain multiple hook changes, which may increase the mental burden compared to maintaining one class. Only the way that suits your business is the best solution.


Reference https://juejin.cn/post/6961000054018539550