React High-order Components Learn more about React high-order components and learn more about React high-order components.

What are higher-order components?

HOC is an advanced technique used in React to reuse component logic. HOC itself is not part of the React API; it is a design pattern based on the composite features of React.

Specifically, a higher-order component is a function that takes a component and returns a new component.

const EnhancedComponent = higherOrderComponent(WrappedComponent);
Copy the code

A component converts props to UI, and a higher-order component converts a component to another component.

HOC is common in third-party libraries for React, such as Redux’s Connect and Relay’s createFragmentContainer.

Use HOC to solve crosscutting concerns

Components are the basic unit of code reuse in React. But you’ll find some patterns that don’t work with traditional components.

For example, consider a CommentList component that subscribes to an external data source to render a list of comments:

class CommentList extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); This.state = {// suppose "DataSource" is a global DataSource variable comments: datasource.getcomments ()}; } componentDidMount () {/ / subscribe to change the DataSource. AddChangeListener (enclosing handleChange); } componentWillUnmount () {/ / remove to subscribe to the DataSource. RemoveChangeListener (enclosing handleChange); } handleChange() {this.setState({comments: datasource.getComments ()}); } render() { return ( <div> {this.state.comments.map((comment) => ( <Comment comment={comment} key={comment.id} /> ))} </div> ); }}Copy the code

Later, a component, BlogPost, was written to subscribe to a single BlogPost that follows a similar pattern:

class BlogPost extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { blogPost: DataSource.getBlogPost(props.id) }; } componentDidMount() { DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { DataSource.removeChangeListener(this.handleChange); } handleChange() { this.setState({ blogPost: DataSource.getBlogPost(this.props.id) }); } render() { return <TextBlock text={this.state.blogPost} />; }}Copy the code

Commentlists are different from blogposts – they call different methods on the DataSource and render different results. But most of their implementations are the same:

  • Add a change listener to the DataSource at mount time.
  • Inside the listener, setState is called when the data source changes.
  • On uninstall, remove the listener.

It is conceivable that this pattern of subscribing to the DataSource and calling setState would happen again and again in a large application. We need an abstraction that allows us to define this logic in one place and share it across many components. This is where higher-order components excel.

For components that subscribe to a DataSource, such as CommentList and BlogPost, we can write a create component function. The function takes as one of its arguments a child component that uses the subscription data as a prop. Let’s call the withSubscription function:

const CommentListWithSubscription = withSubscription(
    CommentList,
    (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
    BlogPost,
    (DataSource, props) => DataSource.getBlogPost(props.id)
);
Copy the code

The first parameter is the wrapped component. The second parameter returns the data we need via the DataSource and the current props.

When rendering CommentListWithSubscription and BlogPostWithSubscription CommentList and BlogPost will pass a data prop, It contains the latest data retrieved from the DataSource:

// This function accepts a component... function withSubscription(WrappedComponent, selectData) { // ... And returns another component... return class extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { data: selectData(DataSource, props) }; } componentDidMount() { // ... Responsible for subscription related operations... DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { DataSource.removeChangeListener(this.handleChange); } handleChange() { this.setState({ data: selectData(DataSource, this.props) }); } render() { // ... And render the wrapped component with the new data! Return <WrappedComponent data={this.state.data} {... this.props} />; }}; }Copy the code

Note that HOC does not modify the incoming component, nor does it use inheritance to replicate its behavior. Instead, HOC makes up new components by wrapping them in container components. HOC is a pure function with no side effects.

The wrapped component receives all the prop from the container component, as well as a new data prop for Render. HOC doesn’t need to care about how or why the data is being used, and the packaged component doesn’t need to care about how the data came from.

Because withSubscription is a normal function, you can add or remove parameters as needed. For example, you might want to make the name of data Prop configurable to further isolate HOC from wrapping components. Or you can accept a shouldComponentUpdate parameter, or a parameter to configure the data source. All of this is possible because HOC can control how components are defined.

As with components, the contract between withSubscription and wrapped components is based entirely on the props passed between them. This dependency makes it easy to replace HOC, as long as they provide the same prop for the packaged components. This is useful when, for example, you need to switch to another library to retrieve data.

Do not change the original component. Using the combination

Don’t try to modify the component prototype (or otherwise change it) in HOC.

function logProps(InputComponent) { InputComponent.prototype.componentDidUpdate = function(prevProps) { console.log('Current props: ', this.props); console.log('Previous props: ', prevProps); }; // Returns the original input component, indicating that it has been modified. return InputComponent; } // The enhanced component gets log output each time the logProps is called. const EnhancedComponent = logProps(InputComponent);Copy the code

There are some bad consequences. One is that input components can no longer be used in the way they were before HOC was enhanced. Worse, if you augment it with another HOC that also modifies componentDidUpdate, the previous HOC will fail! At the same time, HOC cannot be applied to functional components that have no life cycle.

Modifying HOC for incoming components is a poor abstraction. Callers must know how they are implemented to avoid conflicts with other HOC.

HOC should not modify incoming components. Instead, it should use a composite approach by wrapping components in container components:

function logProps(WrappedComponent) { return class extends React.Component { componentDidUpdate(prevProps) { console.log('Current props: ', this.props); console.log('Previous props: ', prevProps); } render() {// Wrap the input component in a container without modifying it. Good! return <WrappedComponent {... this.props} />; }}}Copy the code

Convention: Pass unrelated props to the wrapped component

HOC adds features to components. They should not change their agreements drastically. The component returned by HOC should maintain a similar interface to the original component.

HOC should pass through props that have nothing to do with itself. Most HOC should include a render method like this:

Render () {// Filter out props not on this HOC, and do not pass const {extraProp,... passThroughProps } = this.props; // Inject props into the wrapped component. // Usually the value of state or instance method. const injectedProp = someStateOrInstanceMethod; Return (<WrappedComponent injectedProp={injectedProp} {... passThroughProps} /> ); }Copy the code

This convention ensures the flexibility and reusability of HOC.

Convention: Maximize composability

Not all HOC is created equal. Sometimes it takes only one argument, the wrapped component:

const NavbarWithRouter = withRouter(Navbar);
Copy the code

HOC can typically receive multiple parameters. In Relay, for example, HOC receives an additional configuration object that specifies the component’s data dependencies:

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

The most common HOC signatures are as follows:

// React Redux 'connect' function const ConnectedComment = connect(commentSelector, commentActions)(CommentList);Copy the code

Learn more about React advanced components

hocFactory:: W: React.Component => E: 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 (WrappedComponent) refers to the wrapped react.component. E (EnhancedComponent) refers to the new HOC with the return type of React.component. W

We intentionally obscure the concept of “parcel” in our definition because it can have one of two different meanings:

  • Props Proxy: HOC to operate on porps passed to WrappedComponent W,
  • Inheritance Inversion: HOC Inheritance

What can you do with higher-order components?

  • Code reuse, logic abstraction

  • Implement render hijacking

  • Abstract and manipulate state and props

  • Refining components (such as adding a life cycle)

The problems that higher-order components can solve can be summarized in the following three aspects:

  • Extracting repetitive code to realize component reuse. Common scenario: Page reuse.
  • Conditional rendering, controlling the rendering logic of components (render hijacking), common scenarios: permission control.
  • Capture/hijack the life cycle of the component being processed, common scenarios: component rendering performance tracking, logging.

Implementation of a high-level component factory

In this section, we will look at two major methods for implementing high-order components in React: Props Proxy and Inheritance Inversion. The two methods cover several ways to wrap WrappedComponent.

Props Proxy (PP)

The simplest implementation of Props Proxy (PP) :

function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { return <WrappedComponent {... this.props}/> } } }Copy the code

HOC returns a React Element of type WrappedComponent in the Render method. We also passed in props that HOC received, which is where the name props Proxy came from.

<WrappedComponent {... this.props}/> // is equivalent to React.createElement(WrappedComponent, this.props, null)Copy the code

In the React reconciliation process, both create a React Element for rendering.

React processes the virtual DOM to be updated to the real DOM, including comparing the old and new virtual DOM and calculating the minimum DOM operation.

What can I do with Props Proxy?
  • Operating props

  • Get the component instance from Refs

  • Abstract the state

  • Wrap the WrappedComponent with other elements

Operating props

You can read, add, edit, and delete props passed to WrappedComponent.

Be careful when deleting or editing important props. You should probably use the namespace to make sure that higher-order props don’t break the WrappedComponent.

Example: Add new props. In this application, the currently logged user can be accessed in WrappedComponent via this.props. User.

function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { const newProps = { user: currentLoggedInUser } return <WrappedComponent {... this.props} {... newProps}/> } } }Copy the code
Access component instances through Refs

You can access this (WrappedComponent instance) by reference (ref), but to get the reference, WrappedComponent also needs an initial render, This means that you need to return the WrappedComponent element in HOC’s Render method, let React start its consistency processing, and you’ll get a reference to the WrappedComponent instance.

Example: How to access the instance methods and the instance itself via 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

The Ref callback is executed when the WrappedComponent renders, and you get a reference to the WrappedComponent. This can be used to read/add props to an instance and call its methods.

To extract the state

You can extract state by passing in props and callback functions, similar to smart Component and dumb Component.

Example of extracting state: Extracting the value of input and the onChange method. This simple example is not very conventional, but it is illustrative enough.

function ppHOC(WrappedComponent) { return class PP extends React.Component { constructor(props) { super(props) this.state = { name: '' } this.onNameChange = this.onNameChange.bind(this) } onNameChange(event) { this.setState({ name: event.target.value }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange } } return <WrappedComponent {... this.props} {... newProps}/> } } }Copy the code

You can use it like this:

@ppHOC class Example extends React.Component { render() { return <input name="name" {... this.props.name}/> } }Copy the code

This input automatically becomes a controlled input.

Wrap the WrappedComponent with other elements

To encapsulate styles, layouts, or other purposes, you can wrap WrappedComponent with other components and elements. The basic approach is to use the parent component (Appendix B), but with HOC you get more flexibility.

Example: Package style

function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { return ( <div style={{display: 'block'}}> <WrappedComponent {... this.props}/> </div> ) } } }Copy the code

Inheritance Inversion

The simplest implementation of Inheritance Inversion (II)

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.

I won’t go into detail about what lifecycle methods can be used for, because it’s a React feature rather than HOC feature. But note that with II you can create new lifecycle methods. To avoid breaking the WrappedComponent, call super.[lifecycleHook].

Reconciliation Process

The React element decision describes what it renders when React performs a conformance process.

React elements come in two types: strings and functions. The React element of the string type represents a DOM node, and the React element of the function type represents a component that inherits react.component.

React elements of function type are parsed into a tree of string type React components in the conformance process (the result is always a DOM element).

This is important because it means that the higher-order components of Inheritance Inversion will not necessarily parse the full subtree.

This is important when learning about Render Highjacking.

What can you do with Inheritance Inversion?
  • Render Highjacking
  • The operating state
Rendering hijacked

It’s called render hijacking because HOC controls the render output of WrappedComponent and can be used for all sorts of things.

By rendering hijack you can:

  • Read, add, edit, and delete props in any React element output by Render
  • Read and modify the React element tree output by Render
  • Conditionally render the element tree
  • Wrap styles in the element tree (as in the Props Proxy)

You cannot edit or add the props of the WrappedComponent instance because the React component cannot edit the props it receives, but you can modify the props of the component returned by the Render method.

As we just learned, type II HOC doesn’t necessarily parse the full subtree, meaning that rendering hijacking has some limitations. As a rule of thumb, with render hijacking you can fully manipulate the element tree returned by WrappedComponent’s Render method. But if the element tree contains a React component of function type, you can’t operate on its children. (Delayed by React consistency processing until actual rendering to the screen)

Example 1: Conditional rendering. When this.props. LoggedIn is true, this HOC will render the WrappedComponent completely. (Assuming HOC receives loggedIn prop)

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render()
      } else {
        return null
      }
    }
  }
}
Copy the code

Example 2: Modify the React component tree output by the Render method.

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

In this example, if the WrappedComponent output has an input at the top, set its value to “May the force be with you.”

You can do all sorts of things here, you can go through the entire element tree and change the props of any element in the element tree.

  • 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.

Example: Debug by accessing props and state of the WrappedComponent.

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

Here HOC wraps the WrappedComponent around other elements and outputs the props and state of the WrappedComponent instance.

named

Wrapping a component in HOC causes it to lose its WrappedComponent name, potentially affecting development and debugging.

The name of HOC is usually prefixed with the name of WrappedComponent. The following code comes from react-redux:

DisplayName = 'HOC(${getDisplayName(WrappedComponent)})' // or class HOC extends... { static displayName = `HOC(${getDisplayName(WrappedComponent)})` ... }Copy the code

GetDisplayName function:

The function getDisplayName (WrappedComponent) {return WrappedComponent. DisplayName | | WrappedComponent. Name | | 'Component' }Copy the code

You don’t actually have to write it yourself, recompose provides this function.

Refer to the article

Learn more about React advanced components

React Advanced Components (HOC)

React High-level Component (HOC) introduction 📖 and practice 💻