Author avatar

Chris Parker

How to Wrap One React Component into Another

Chris Parker

  • Dec 5, 2019
  • 14 Min read
  • 7,575 Views
  • Dec 5, 2019
  • 14 Min read
  • 7,575 Views
Web Development
React

Introduction

In React, higher-order components, or HOCs, are functions that take a component and output a new component after enhancing it in some manner:

1
const EnhancedHOCComponent = hoc(OriginalReactComponent);
javascript

HOCs play a useful role when you have to insert functions, state and supplementary data into components or group components with styling or further JSX. The use of HOCs enables us to only extend the components that are required to be extended while keeping the codebase modular by separating the component implementation and the logic.

The thing to keep in mind here is that React components are designed to be reusable; they should be focused on a single purpose and should contain the logical implementation for that particular purpose. HOCs enable us to stick to these requirements.

The purpose of this guide is to elaborate on what HOCs are and how they are used in React. It also contains examples of their application.

High-Level View Of HOCs

HOC design pattern is applied by many modules as a way of inserting functionality from the module into the components. This is the most prevalent use case of HOCs.

Most widely used packages in React adopt HOCs design pattern, as can be seen in the following examples:

  • react-cookies enable us to store and retrieve cookies from any component, the functions of which are inserted using withcookies() HOC.
  • react-redux gives us connect() HOC, which inserts state and actions as props, enabling components access the Redux store.
  • react-router-dom has a withRouter() HOC that gives components router history, location and nearest matching <Route/>.

HOCs are mostly prefixed with with or get. HOCs prefixed using with are mostly used to inject functionality, while HOCs prefixed using get are mostly used to inject data in the original component.

In general, HOCs are applied to any component at export after our component has been defined.

1
2
3
4
5
import { withSomeFunctions } from 'some-module';
class OriginalReactComponent extends React.Component {
   ...
}
export default withSomeFunctions(OriginalReactComponent);
javascript

It's quite easy to apply a HOC to any component; we just need to import the required HOC function and wrap our original component with the HOC. The implementation of a HOC is also the same as any function.

1
2
3
4
5
6
7
8
9
10
export function withSomeFunctions(OriginalReactComponent) {
   return class extends React.Component {
      // we can make some enhancements here
      ...
      render() {
         //return original react component with additional props
         return <OriginalReactComponent {...this.props} />
      }
   }
}
javascript

Thus, a new class gets created using our original component without updating the original. Actually what happens is:

  • The original component is provided as an input to the HOC.
  • The HOC function outputs a class component with improvements we want to make, like additional lifecycle methods, API endpoints, etc.
  • render() outputs the original component with any extra props we may want to add or any JSX we may want include.

It is considered bad coding practice to update the original component or to change its prototype. Instead, best practice is to follow a composition pattern where our original component gets wrapped inside the new container class—hence the following terminology:

  • Wrapped component is the original component.
  • Container component is the new class that is defined in the HOC.

Note that the HOC always returns a class component. If we pass any functional component into withMyFunction(), a new class component will be returned. The instructions on implementing functional HOCs are documented in this guide.

Since HOCs are functions, we can define them using export functions in a separate file and import it as any other function:

1
2
3
4
5
6
7
8
9
10
// src/hocexample/index.js
import React from 'react';
export function withSomeFunctions(WrappedComponent) {
   return class extends React.Component {
      ...
   }
}
// src/components/OriginalReactComponent.js
import { withSomeFunctions } from '../hocexample';
...
javascript

Manipulation of Props

The examples above show that passing extra props into our wrapped components can be done by using spread operator in our return statement:

1
2
3
render() (
<WrappedComponent {...this.props} />
);
javascript

This is an important first step to ensure our <WrappedComponent /> receives any new props that are passed into our HOC via JSX.

Passing props through JSX is not the only option, though. Let's say the aim of our HOC is to insert some functions for performing a specific task, e.g., to provide API calls to a web service. For this use case, we’ll import the functions and then include them as props inside our <WrappedComponent />:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/hocexample/index.js
import React from 'react';
import { f1, f2 } from '../someFunctions';
export function withSomeFunctions(WrappedComponent) {
   return class extends React.Component {
      
      render() {
         const updatedProps = {
            f1: f1,
            f2: f2
         };
       
         return <WrappedComponent {...this.props} {...updatedProps} />
      }
   }
}
javascript

On the other hand, if we have some props that we do not want to pass through to <WrappedComponent />, we can overcome this issue by filtering out non-required props and use spread operator again to group our remaining props:

1
2
3
4
render() {
   const { notNeededProp, someOtherProps, ...finalProps } = this.props;
   return <WrappedComponent {...finalProps} />
}
javascript

State Abstraction

Another use of HOCs is for inserting state into components. A React class component with state has similar functionality. The following example inserts state for storing a first name. It also has a handler function to update the same.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/hocexample/index.js
import React from 'react';
export function withSomeFunctions(WrappedComponent) {
   return class extends React.Component {
      constructor(props) {
         super(props);			
         this.state = {
            firstName: ''
         };
     }
      onFirstNameUpdate = (event) => {
         this.setState({ firstName: event.target.value });
      }
      render() {
         return 
           <WrappedComponent 
             {...this.props}
             firstName={this.state.firstName}
             onFirstNameUpdate={this.onFirstNameUpdate}
           />
      }
   }
}
javascript

Instead of defining a newProps object, we just pass our state related props directly to WrappedComponent. HOCs can be used to inject values from a global store.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor(props) {
   super(props);
   this.state = {
      firstName: store.get('firstName') || {}
   };
}
...
render() {
   return 
      <WrappedComponent 
        {...this.props}
        {...this.state}
        onFirstNameUpdate={this.onFirstNameUpdate}
       />
   }
}
javascript

Reusability When Using HOCs

To avoid repetition, the official React docs have highlighted HOC abstraction of subscribing to a data source and then inserting the retrieved data into a wrapped component each time there is a change in the data, in addition to saving it in HOC’s state. The subscription to a data source itself is managed by HOC's lifecycle methods.

The HOC is called withSubscription(), which enables to pass a DataSource object to retrieve whatever the wrapped component concerns, e.g., a news post, a list of comments, live news feed, some logs, etc. The basis of the DataSource is not really documented in the official React docs, and it is not very clear how it retrieves the data and listens to the changes (via polling, web sockets, etc).

But the interesting part is that a second argument is passed to our HOC, which is a function that retrieves data from an API:

1
2
3
4
5
6
// a data source for getting list of comments
(DS, props) => DS.getListOfComments()
//a data source for getting news post
(DS, props) => DS.getNewsPost(props.id)
//a data source for getting some logs
(DS, props) => DS.getLogs(props.id)
javascript

Thus, a function definition is inserted into our HOC to retrieve data.

The componentDidMount and componentDidUnmount methods have been presented to handle the initialization as well as termination of our listener, and also a handleChange method that is invoked every time our data changes, therefore committing to the state of our component and rendering our wrapped component again.

This use case might be a bit hard to understand. These are the key points:

  • Although single argument is the best option in most scenarios, we have the option to insert multiple arguments into a HOCs.
  • Functions can also be a part of the arguments that we pass into a HOC, that can be invoked within the HOC. This can be very handy when we want to link our HOCs to web services, determining the data being given to our wrapped components.
  • Wrapped components can also be grouped with lifecycle methods. Managing data subscriptions or listening to events are some good examples of how lifecycle methods can be used within a HOC.

Utilities

Component metadata is a less complex use case for HOC. It involves retrieving attributes of a wrapped component that can be inserted as props afterwards.

react-measure or react-with-available-width are some good examples of HOCs that measure our component width, height, bounds, etc. Both of these packages have an onResize method, enabling us to respond to changes in any of the metrics mentioned above. These can be very useful for advanced UX customization, like reacting to window scrolling, resizing, etc.

Higher Order Functions or HOFs

Based on the discussion so far, we see that a HOC takes in a component and returns a new one. But this concept can be explored further. Let's say we have a function which would return another function and we can then use this to return our improved wrapped component, hereby adding another level of functionality to the HOC.

Redux’s connect() does the same thing, as can be seen in the following syntax:

1
2
const MyEnhancedComponent = 
  connect(mapStateToProps, matchDispatchToProps)(WrappedComponent);
javascript

connect() returns a function and is also a function itself, and the single argument passed into that resulting function is the WrappedComponent.

Based on the above definition, connect() can be called a higher order function, or HOF; the output is actually an enhanced function with mapStatetoProps and matchDispatchtoProps, which then returns a HOC.

To elaborate this further, we are giving configuration options to higher order function to determine what is inserted into our enhanced component.

HOFs and HOCs’ Single-Argument Function

If we inspect connect()’s method, we see 4 optional params:

1
function connect(mapStateToProps, mapDispatchToProps, mergeProps, options)
javascript

The reason we have the extra functional layer is because our HOC can remain a single-argument function, making composition easier when combining HOCs together:

1
const MyEnhancedComponent = withRouter(connect(mapState, mapDispatch)(WrappedComponent))
javascript

There is no restriction in adding more parameters to our HOCs for additional configuration as that is entirely valid syntax:

1
function someHoc(WrappedComponent, config)
javascript

However, we are sacrificing the Component => Component signature which is generally associated with the HOC.

HOCs With Functional Components

The discussion so far has been around HOCs that return class components. With the introduction of functional components and React Hooks taking more priority than ever before, the question may arise whether HOCs can work with functional components too.

To implement this, we just need to return an improved function instead of returning a new class. This could be more limiting, though, than returning class components, as we’ll no longer be able to use the lifecycle methods.

However, we'll still have React Hooks. useEffect and useState can be used instead of using lifecycle methods and this.state respectively.

A demonstration of improving a functional component with useState hook is shown below. It enables the wrapped component to increase the counter provided by the HOC. The wrapped components are then inserted with counter and setCounter hook by the HOC itself.

1
2
3
4
5
6
7
8
9
10
11
//Functional HOC using the useState hook
import React, { useState } from 'react';
function withCounterState(WrappedComponent) {
   return function (...props) {
      const [counter, setCounter] = useState(0);
  
      props['counter'] = counter;
      props['setCounter'] = setCounter;
      return <WrappedComponent {...props} />;
   }
}
javascript

As shown above, the wrapped component will now be able to increase the counter provided by withCounterState(). Props will get passed as an argument here vs. using this.props when using a class-based component.

Now the <WrappedComponent /> component has become very simple, as shown below:

1
2
3
4
5
6
7
8
9
10
11
const WrappedComponent = (props) =>  {
   const {counter, setCounter} = props;
   return(
     <div>
        <h1>Functional Component Example for Counter</h1>
        <p>We clicked {counter} times</p>
        <button onClick={() => setCounter(counter + 1)}>
           Increase counter
        </button>
     </div>);
};
javascript

Finally, we apply the HOC to <WrappedComponent /> like this:

1
const MyEnhancedWrappedComponent = withCounterState(Wrapped);
javascript

As functional components generally have less overhead compared to class components, they are preferred and we can switch from a class component HOC.

Conclusion

While most of the points we discussed here involve the benefits of HOCs, there are some caveats we should be aware of, one of them being that HOCs cannot be invoked from a component’s render method.

There are also some other edge cases mentioned in the official React docs which most developers might not need to consider, but it's still recommended to be aware of those.

9