preface

Recently, I learned the encapsulation of React. Although HOC and Render Props were also used in daily development, I still didn’t understand everything from inheritance to combination, static construction to dynamic rendering, so I just took time to systematically arrange. If there is any mistake, please feel free to spray ~~

example

Here’s an example from React. I’ll use different encapsulation methods to try to reuse code.

Components in React are the primary unit of code reuse, but it’s not clear how to share state or encapsulate the behavior of one component into other components that require the same state. For example, the following component tracks mouse position in a Web application:

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <h1>Move the mouse around!</h1>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}
Copy the code

Displays the (x, y) coordinates of a p component as the mouse moves across the screen.

The question now is: How do we reuse behavior in another component? In other words, if another component needs to know the mouse position, can we encapsulate this behavior so that it can be easily shared between components?

Since components are the most fundamental unit of code reuse in React, trying to refactor a portion of the code now encapsulates the behavior we need elsewhere in the component.

// The <Mouse> component encapsulates the behavior we need...
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%'}} onMouseMove={this.handleMouseMove}> {/* ... but howdo we render something other than a <p>? */}
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse />
      </div>
    );
  }
}
Copy the code

The component now encapsulates all the behavior about listening for Mousemove events and storing mouse (X, Y) positions, but it still remains truly reusable.

For example, suppose we now have a component that renders a picture of a cat on the screen following the mouse. We might use <Cat mouse={{x, y}} prop to tell the component the coordinates of the mouse so that it knows where the picture should be on the screen.

First, you might try rendering components in an internal render method like this:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          We could just swap out the <p> for a <Cat> here ... but then
          we would need to create a separate <MouseWithSomethingElse>
          component every time we need to use it, so <MouseWithCat>
          isn't really reusable yet. */} 
        ); } } class MouseTracker extends React.Component { render() { return ( 
      

Move the mouse around!

); }}
Copy the code

This approach worked for our specific use case, but we failed to achieve our goal of truly encapsulating behavior in a reusable way. Now, every time we want to use mouse position in a different use case, we have to create a new component that renders different content for that use case (like another key

).

Mixin

Mixins concept

React Mixin wraps generic shared methods as Mixins and then injects component implementations into them. It’s actually not officially recommended anymore, but you can still learn why it was abandoned, starting with the API. React mixins can only be used with React.createclass (), as follows:

var mixinDefaultProps = {}
var ExampleComponent = React.createClass({
    mixins: [mixinDefaultProps],
    render: function(){}
});
Copy the code

Mixins implementation

// Mixin const mouseMixin = {getInitialState() {
    return {
      x: 0,
      y: 0
    }
  },
  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }
}

const Mouse = createReactClass({
  mixins: [mouseMixin],
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    )
  }
})

const Cat = createReactClass({
  mixins: [mouseMixin],
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <img src="/cat.jpg" style={{ position: 'absolute', left: this.state.x, top: this.state.y }} alt="" />
      </div>
    )
  }
})
Copy the code

The problem of a Mixin

However, why are mixins not recommended? To sum up, there are the following three points

1. Mixin introduces implicit dependencies such as:

You might write a stateful component, and then your colleague might add a mixin that reads that state. Within a few months, you may need to move this state to the parent component to share it with the sibling component. Will you remember to update mixins to read items? What if other components now use this mixin?

2. Mixin causes name conflicts.

You define getSomeName in this Mixin, and another Mixin defines the same name getSomeName, causing a conflict.

Mixins lead to complex snowballs

Over time and as your business grows, you make more and more changes to a Mixin, and you end up with a Mixin that is difficult to maintain.

4. Embrace ES6, ES6 classes do not support mixins

HOC

HOC concept

HOC is an advanced technique in React that reuses component logic. But the higher-order components themselves are not the React API. It is only a pattern, which is inevitably generated by the combinatorial nature of React itself and a pattern generated in the development of react community. A higher-order component is a function whose input parameter is a function and return is a function. As the name implies, a higher-order component is a function whose input parameter is a component and return is a component.

const EnhancedComponent = higherOrderComponent(WrappedComponent);
Copy the code

HOC implementation

There are two ways to use higher-order components in the community:

W (WrappedComponent) refers to the wrapped react.component. E (EnhancedComponent) refers to the new HOC with the return type of react.component. W (WrappedComponent) refers to the wrapped react.component. E (EnhancedComponent) refers to the new HOC with the return type of React.component. W

  • Props Proxy: HOC operates on porps passed to WrappedComponent W.
  • Inheritance Inversion: HOC inherits WrappedComponent W.

Using the Props Proxy as an example, let’s start with the Props Proxy:

class Mouse extends React.Component {
  render() {
    const { x, y } = this.props.mouse
    return (
      <p>The current mouse position is ({x}, {y})</p>
    )
  }
}

class Cat extends React.Component {
  render() {
    const { x, y } = this.props.mouse
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: x, top: y }} alt="" />
    )
  }
}


const MouseHoc = (MouseComponent) => {
  returnclass extends React.Component { constructor(props) { super(props) this.handleMouseMove = this.handleMouseMove.bind(this)  this.state = { x: 0, y: 0 } } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }) }render() {
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
          <MouseComponent mouse={this.state} />
        </div>
      )
    }
  }
}

const EnhanceMouse = MouseHoc(Mouse)
const EnhanceCat = MouseHoc(Cat)
Copy the code

So what can we do in the Props Proxy mode of Hoc?

To operate MouseHoc, let’s say we need to pass in a prop to Mouse or Cat. Then we can add, delete, and check the Props in HOC as follows:

const MouseHoc = (MouseComponent, props) => {
  props.text = props.text + '---I can operate props'
  return class extends React.Component {
    ......
    render() {
      return (
        <div style={{ height: '100%'}} onMouseMove={this.handleMouseMove}> <MouseComponent {... props} mouse={this.state} /> </div> ) } } } MouseHoc(Mouse, { text:'some thing... '
})
Copy the code

Access component instances through Refs

function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }

    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return<WrappedComponent {... props}/> } } }Copy the code

Extracting state is our example.

<MouseComponent mouse={this.state} />
Copy the code

The parcel WrappedComponent

<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
    <MouseComponent mouse={this.state} />
</div>
Copy the code

The other HOC mode is Inheritance Inversion, but this mode is rare, the simplest example is as follows:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}
Copy the code

As you can see, the returned HOC class (Enhancer) inherits the WrappedComponent. It is called an Inheritance Inversion because the WrappedComponent is inherited by Enhancer, not the other way around. In this way, their relationships look like inverse. Inheritance Inversion allows HOC to access WrappedComponent via this, meaning it can access state, props, component lifecycle methods, and Render methods.

So in our example it looks like this:

class Mouse extends React.Component {
  render(props) {
    const { x, y } = props.mouse
    return (
      <p>The current mouse position is ({x}, {y})</p>
    )
  }
}

class Cat extends React.Component {
  render(props) {
    const { x, y } = props.mouse
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: x, top: y }} alt="" />
    )
  }
}


const MouseHoc = (MouseComponent) => {
  return class extends MouseComponent {
    constructor(props) {
      super(props)
      this.handleMouseMove = this.handleMouseMove.bind(this)
      this.state = { x: 0, y: 0 }
    }

    handleMouseMove(event) {
      this.setState({
        x: event.clientX,
        y: event.clientY
      })
    }
    render() {
      const props = {
        mouse: this.state
      }
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
          {super.render(props)}
        </div>
      )
    }
  }
}

const EnhanceMouse = MouseHoc(Mouse)
const EnhanceCat = MouseHoc(Cat)
Copy the code

Again, in mode II, what can we do?

Render hijacking because render() returns the JSX compiled object as follows:

You can manually modify the tree to achieve some desired effects, but this is not usually used:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      const elementsTree = super.render()
      let newProps = {};
      if (elementsTree && elementsTree.type === 'input') {
        newProps = {value: 'may the force be with you'}
      }
      const props = Object.assign({}, elementsTree.props, newProps)
      const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
      return newElementsTree
    }
  }
}
Copy the code

The operating state

HOC can read, edit, and remove the state of the WrappedComponent instance, or add more state to it if you want. Remember, this will mess up the state of the WrappedComponent, so you might break something. To limit HOC reading or adding state, add state in a separate namespace, not mixed with WrappedComponent state.

export function IIHOCDEBUGGER(WrappedComponent) {
  return class II extends WrappedComponent {
    render() {
      return (
        <div>
          <h2>HOC Debugger Component</h2>
          <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
          <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
          {super.render()}
        </div>
      )
    }
  }
}
Copy the code

Why have classes instead of using inheritance to return using HOC

Some people may wonder why there is Class and not use inheritance to return to use HOC. Here is a better answer recommended by Zhihu

OOP and FP are not incompatible, so mixing them together is fine, and many libraries based on FP ideas also need OOP. Why React promotes HOC and composition? My understanding is that React wants components to be encapsulated according to the idea of minimal availability. Ideally, a component only does one thing and does it well, DRY. In OOP principles, this is called the single responsibility principle. If you want to enhance a component, you should first think about what capabilities the enhanced component needs, which components provide those capabilities, and then put those components together.

A-related functions in D are assigned to A in D, and b-related functions in D are assigned to B in D. D is only responsible for maintaining the relationship between A, B and C. In addition, it can also provide additional items to enhance components.

There’s nothing wrong with inheritance. Remember, React is just a recommendation, not a restriction. Extending components with inheritance is fine, and there are also scenarios. For example, there is a Button component, which is just a package of Button, and we call it Button. However, according to product requirements, buttons in many places are with an icon, so we need to provide an IconButton. At this point, it is possible to extend through inheritance and combine another separate component, which we will call Icon. The function of displaying Icon is handed over to the Icon component, and the function of the original button continues. For this type of component extension, I think inheritance is ok, flexibility, reusability is still there. However, before extending with inheritance, consider whether the new component is of the same type and responsibility as the inherited component. If so, inherit; if not, combine. How to define the same class, back to the Button example above, the so-called same class, that is, I directly replace the Button with IconButton directly, do not change the other code, the page can still be rendered normally, functions can be used normally, can be considered the same class, in OOP, this is called the Richter substitution principle.

What problems will inheritance bring? According to my practical experience, although excessive use of inheritance brings convenience to coding, it is easy to lead to out-of-control code, component expansion, and reduce component reusability. For example, there is a list component, let’s call it ListView, that scrolls up and down a set of items, and then one day the requirements change, and PM says, I want this ListView to bounce back like iOS does. Ok, extend the ListView with inheritance, add the rebound effect, task closed. The next day PM came to the door, I hope all the scroll up and down can support the rebound effect, at this time muddleforce, how to do? Copy the ListView rebound code again, okay? This is contrary to the DRY principle, and it is possible that the springback effect will be slightly different due to the influence of code from other places. If the PM wants to upgrade this springback effect, it will have to be changed. What’s the best way to deal with this scenario? Use combination to encapsulate a Scroller with rebound effect. ListView is regarded as the combination of Scroller and Item container components. If other parts need to use rolling, a Scroller can be directly installed. Ideally, of course, springback should also be made into a component, SpringBackEffect, separate from Scroller, so that wherever springback is needed, the SpringBackEffect component is added, which is why composition takes precedence over inheritance.

When the page is simple, whether it is composed, inherited, maintainable, able to quickly respond to the needs of the iteration is good, in which way to achieve it does not matter. But if you’re working on a large project with many components, or if the page is being maintained by multiple teams, consider the potential conflicts in the collaboration and use constraints to close the pits. Composition is one of the practices that ensures that components are sufficiently reusable, flexible, and DRY compliant.

Mixin vs. HOC

Mixins are like their name, they are mixed into components, it is difficult to manage a component mixed with multiple mixins, like a box, we cram all kinds of things (features) into the box, it is definitely difficult to clarify the context. HOC is like a decorator, layer by layer on the outside of the box, and when we want to extract a layer or add a layer, it’s very easy.

The provisions of the HOC

Pass through properties of unrelated props to the wrapped component. A higher-order component should pass through properties of props that are not related to its specific concerns.

render() {// Filter out props properties specific to this component, // should not be passed const {extraProp,... passThroughProps } = this.props; / / added props attribute to the wrapped component, these are generally state values or / / instance methods const injectedProp = someStateOrInstanceMethod; Pass the props attribute to the wrapped componentreturn( <WrappedComponent injectedProp={injectedProp} {... passThroughProps} /> ); }Copy the code

Maximized combinativity

// Don't do that... Const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // You can use a function composition tool for // compose(f, g, h) and (... args) => f(g(h(... Args))) is the same as const enhance = compose(// these are high-order components withRouter with a single argument, connect(commentSelector) ) const EnhancedComponent = enhance(WrappedComponent)Copy the code

The wrapper shows the name for easy debugging

The most common technique is for the package to display the name to the wrapped component. So, if your high-order component name is withSubscription and the display name of the wrapped component is CommentList, then the display name is withSubscription

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)}) `;return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
Copy the code

HOC alert

  • Do not use higher-order components within the Render method, as each higher-order component returns a different component, resulting in unnecessary rendering.
  • Static methods must be copied.

Problems with HOC:

  • When there is multiple HOC, you don’t know where the Props are coming from.
  • As with mixins, there is an override problem if there are props with the same name, and React does not report an error.
  • There are many more layers (useless, empty components) in the JSX hierarchy that make debugging difficult.
  • HOC is static build, which means that a component is regenerated, that is, a new component is returned and not rendered immediately, that is, the lifecycle functions defined in the new component are executed only when the new component is rendered.

Render Props

Render Props concept

Render Props, as the name suggests, is also a solution for stripping logic code for reuse and improving component reusability. In the component being reused, a function that takes an object and returns a child component passes the object in the function argument as props to the newly generated component through a property called “render” (the property name may not be Render, as long as the value is a function).

Render Props used

Take a look at the initial example in use of render props:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}
Copy the code

Advantages of Render Props

  • Don’t worry about where the Props are coming from, it can only be passed from the parent component.
  • Don’t worry about naming props.
  • The Render props are built dynamically.

Dynamic build and static build

React officially advocates dynamic composition, but HOC is actually a static build. For example, if we need to render a Cat or Dog component based on a field in Mouse, using HOC would look like this:

const EnhanceCat =  MounseHoc(Cat)
const EnhanceDog =  MounseHoc(Dog)
class MouseTracker extends React.Component {
  render() {
    return( <div> isCat ? <EnhanceCat /> : <EnhanceDog /> </div> ); }}Copy the code

As you can see, we had to build the Cat and Dog components statically in advance

Let’s say we use Render props:

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={(mouse, isCat) => (
          isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} />
        )}/>
      </div>
    );
  }
}
Copy the code

Obviously, when we build dynamically, we have more flexibility and we can leverage the lifecycle better.

Disadvantages of Render Props

SCU cannot be used for optimization. Please refer to the official documentation for details.

conclusion

Aside from abandoned mixins and Hooks that are not yet fixed, the community’s current code reuse schemes are HOC and Render Props. Personally, I prefer Render Props for multi-layer combinations or dynamic renderings, while for simple operations such as performing on every View, Such as buried points, title Settings, etc., or high performance requirements such as a large number of forms can use HOC.

reference

Function as Child Components Not HOCs React High-order Components and render props are there different scenarios, or is it more of a personal preference? React High-order components — I don’t Use High-order components anymore why does React advocate HOC and composition instead of inheritance to extend components? React Render Props use Render Props. Render Props