A higher-order component is a technique that is available to us because of React’s compositional nature. Specifically, a higher-order component is a function that takes a component as a parameter and returns a new component.

const EnhancedComponent = higherOrderComponent(myComponent)

Or

const IronMan = withSuit(TonyStark)

This new component called EnhancedComponent will include all of the original component’s functionality along with features the higher-order component includes.

Why is there a need for higher-order components in React?

Higher-Order components in React or HOCs are a useful pattern to re-use common code logic across multiple components. This option is more favorable than lifting the state when you are working with multiple components that you want to share the code with. I will demonstrate this in a code example with the fitness logger app.

Simple app for logging Fitness activity

I need an app that will track my different fitness activities and the amount of time per activity. For example: [ {Running: 1, Football: 3} ] Finished code example can be found here.

This data model is an array of objects where 1 and 3 represent hours by that activity. If the activity has an existing entry already logged then I want to sum the hours of this entry to the existing entry already available. My fitness logger will allow 2 different ways to log your data. One component has predefined activities and time amounts called FurnishedLogEntry.js. In the other component, the user has more freedom to enter any activity they want with free text. I’ll call this component the ManualLogEntry.js

App.js

import "./App.css";
import FurnishLogEntry from "./Components/FurnishLogEntry";
import ManualLogEntry from "./Components/ManualLogEntry";
function App() {
  return (
    <div className="App">
      <h3>Enter your fitness hours</h3>
      <header className="App-header">
        <ManualLogEntry />
        <FurnishLogEntry />
      </header>
    </div>
  );
}

export default App;

FurnishedLogEntry.js

import React, {useState} from 'react'

const FurnishLogEntry = () => {
    const [healthStats, setHealthStats] = useState([]);

    const buildNewHealthStatEntry = (activity, hours) => {
        const newHealthStat = createNewHealthStat(activity, hours)
        addHealthStat(newHealthStat);
    }

    const createNewHealthStat = (name, hours) => {
        //build and return the health stat object. Set new or existing flag accordingly
        ...
    };

    const addHealthStat = newHealthStatEntry => {
        //update state accordingly with new or existing stat entry
        ...
    }

    return (
        //return some JSX ...
    )
}
export default FurnishLogEntry

ManualLogEntry.js

import React, {useState} from 'react'

const ManualLogEntry = () => {
    const [hours, setHours] = useState(0)
    const [activity, setActivity] = useState('');
    const [healthStats, setHealthStats] = useState([]);

    const handleChange = ( e ) => {
        if(e.target.classList.contains('activity')){
            setActivity(e.target.value)
            return
        }
        //make sure no letters are entered
        if(/^\d*\.?\d*$/.test(e.target.value)){
            setHours(e.target.value)
        }
    }

    const buildNewHealthStatEntry = (activity, hours) => {
        //same logic in FurnishedLogEntry
    }

    const createNewHealthStat = (name, hours) => {
        //same logic in FurnishedLogEntry
    };

    const addHealthStat = newHealthStatEntry => {
        //same logic in FurnishedLogEntry
    }

    return (
        //return some JSX
    )
}

export default ManualLogEntry

Redundant code in these components.

You should have noticed that 3 functions are the same in both components. The function expressions addHealthStatcreateNewHealthStat, and buildNewHealthStatEntry making them good candidates to be moved into a HOC.

buildNewHealthStatEntry is the parent function that builds the js object to be added or modified. createNewHealthStat creates a new health stat entry and checks if the same activity is in the list already. If we find an existing entry then the new entry’s hours are added to the existing entry. Otherwise, we will add the new health entry to the list. addHealthStat receives the new entry from createNewHealthStat and updates the components state accordingly.

How do I create a functional HOC?

We can create a functional HOC in 5 steps.

  1. Create the HOC component file with the prefixed naming convention with. This component will be called withHealthCounter.js
  2. The HOC must return the original component that was passed in
  3. Pass through the given props.
  4. Import the HOC into the original component
  5. Export the original component being passed into the HOC
    export default withHealthCounter(ManualLogEntry)

Add HOC to our fitness logger app

Now that we know what actions we need to take, let’s apply those changes to our app. First I will create the HOC and share the addHealthStat and buildNewHealthStatEntry functions.

Here is a HOC called withHealthCounter that handles the health stats-related functionality and provides a callback to the wrapped components to build health stat entries.

import React, { useState } from "react";

const withHealthCounter = (WrappedComponent) => (props) => {
  //each component will have it's own instance of local state
  const [healthStats, setHealthStats] = useState([]);

  //shared functions for both components
  const buildNewHealthStatEntry = (activity, hours) => {
    const newHealthStat = createNewHealthStat(activity, hours);
    addHealthStat(newHealthStat);
  };

  const createNewHealthStat = (name, hours) => {
    const existingEntry = healthStats.find((item) => item.name === name);
    if (existingEntry) {
      return {
        ...existingEntry,
        new: false,
        hours: existingEntry.hours + hours,
      };
    }
    return { name: name, hours: hours, id: healthStats.length, new: true };
  };

  const addHealthStat = (newHealthStatEntry) => {
    newHealthStatEntry.new
      ? setHealthStats((prevState) => [{ ...newHealthStatEntry }, ...prevState])
      : setHealthStats((prevState) => [
          newHealthStatEntry,
          ...prevState.filter((x) => x.id !== newHealthStatEntry.id),
        ]);
  };
  // Step 2.  Return the original component with the new props
  return (
    <>
      <WrappedComponent
        healthStats={healthStats}
        //I only need to share 2 HOC props. The enhanced components
        //just need access to buildNewHealthStat and title props
        buildNewHealthStatEntry={buildNewHealthStatEntry}
        title={props.title}
        // Step 3 Very IMPORTANT return any props from the original component
        {...props}
      />
    </>
  );
};

export default withHealthCounter;

With this new file addition to my project, I have completed steps 1, 2, and 3.

Step 1: creating a new file using the proper naming convention with

Step 2: is complete with returning the WrappedComponent.

Step 3: This step is very important!! Pass through the given {...props}. This prevents any props, sent by the user, from being swallowed by the higher order components in React.

I added the functions addHealthStatcreateNewHealthStat, and buildNewHealthStatEntry to the HOC. buildNewHealthStatEntry is the only function the enhanced component needs because it already calls createNewHealthStat and addHealthStat. This hides complexity from the user similar to the facade pattern. Now any component that is passed into this HOC will get the added functionality of the buildNewHealthStatEntry. aka… the WrappedComponent

Let’s use the HOC now

If you have been keeping track, we still need to complete steps 4, and 5 of our checklist.

Step 4: Import the HOC. Piece of cake. I just need to add the import to both FurnishLogEntry.js and manualLogEntry.js

import withHealthCounter from "./SharedCode/WithHealthCounter";

Step 5: Export the component that we received from the HOC while passing in the component we want enhanced. This looks a little different from most exports just because we are exporting the result of a function call. The function being the HOC. Again, this needs to be added to both FurnishLogEntry.js and manualLogEntry.js

export default withHealthCounter(ManualLogEntry);

Now that the HOC is importing and exporting everything correctly we can remove the duplicated code and just use the code from the HOC instead. Here is the updated code for the wrapped components.

Our FurnishLogEntry component might look something like this:

import React from 'react';
import withHealthCounter from "./SharedCode/WithHealthCounter";

const FurnishLogEntry = (props) => {
    const { buildNewHealthStatEntry } = props;
    const getSelectOptions = (values, activity) => {
        return (
            <select
              name={activity}
              onChange={({ currentTarget: { name, value } }) => buildNewHealthStatEntry(name, +value)}
              className={`${activity}-hours`}>
                {values.map(item => <option key={item} value={item}>{item} hours</option>)})
            </select>
        )
    }
    return (
        //return some JSX
    )
}
export default withHealthCounter(FurnishLogEntry);

Our ManualLogEntry component might look something like this:

import React, {useState} from 'react';
import withHealthCounter from './SharedCode/WithHealthCounter';

const ManualLogEntry = props => {
    const [ hours, setHours ] = useState(0)
    const [ activity, setActivity ] = useState('');
    const { buildNewHealthStatEntry } = props;

    const handleChange = ( e ) => {
        if(e.target.classList.contains('activity')){
            setActivity(e.target.value)
            return
        }
        //make sure no letters are entered
        if(/^\d*\.?\d*$/.test(e.target.value)){
            setHours(e.target.value)
        }
    }

    return (
        //return some JSX
    )
}

export default withHealthCounter(ManualLogEntry);

Conclusion

Let’s review what we went over. A higher-order component in React takes a component as a parameter and returns a new component while adding shared code to the new component. HOC are useful for keeping your code base DRY.

I showed an example of this by moving the createNewHealthStataddHealthStat and buildNewHealthStatEntry into the HOC.