Mastering React Higher-Order Components (HOCs)

Relia Software

Relia Software

Huy Nguyen

Relia Software

featured

In React, a higher-order component (HOC) is a function that takes an existing component and returns a new component. Explore further about React HOCs via this blog!

Mastering React Higher-Order Components (HOCs)

Table of Contents

Higher-Order Components (HOCs) are a powerful technique in React that can help you achieve code reusability and improve the maintainability of your applications. In this comprehensive guide, we'll delve into the world of HOCs, explaining what they are, how they work, and the various use cases they offer. We'll also explore the advantages and disadvantages of using HOCs, along with best practices to ensure you leverage them effectively in your React projects.

>> Read more about React:

Understanding Higher-Order Components (HOCs)

In React, a higher-order component (HOC) is a function that takes an existing component and returns a new component. This new component inherits the functionality and behavior of the original component, but it can also add additional functionalities or modify its behavior in some way.

Here's a breakdown of the key points to define HOCs and their purpose:

  • Function that takes a component: The core concept of an HOC is its role as a function. This function accepts another component as its argument.
  • Returns a new component: The HOC doesn't directly modify the original component. Instead, it returns a new component that wraps the original one.
  • Inherits functionality: The new component created by the HOC retains all the functionalities and behavior of the original component.
  • Adds or modifies behavior: The primary purpose of HOCs lies in their ability to add new functionalities or modify the behavior of the wrapped component.

By using HOCs, developers can achieve code reuse and improve the organization of their React applications. They promote a more modular and maintainable codebase. Let's consider a following example.

Imagine a scenario where a user needs a component that increments a counter variable with every onClick event. In this article, we’ll explore Higher-Order Components (HOCs), their syntax, and practical applications. Additionally, we’ll address common challenges associated with working with HOCs.

function ClickCounter() {
  const [clickCount, setClickCount] = useState(0); // Initialize the state with a default value of 0.
  return (
    <div>
      {/* When the button is clicked, increment the value of 'clickCount' */}
      <button onClick={() => setClickCount((prevCount) => prevCount + 1)}>Increment</button>
      <p>Clicked: {clickCount}</p> {/* Render the current value of clickCount */}
    </div>
  );
}

export default ClickCounter;

Our code is functional! However, what if the client desires another component with the same functionality, but triggered by an onMouseOver event?

To achieve this, we’d need to create the following code:

function HoverCounter(props) {
  const [count, setCount] = useState(0);
  return (
    <div>
      {/*If the user hovers over this button, then increment 'count'*/}
      <button onMouseOver={() => setCount((count) => count + 1)}>
        Increment
      </button>
      <p>
        Clicked: {count}
      </p>
    </div>
  );
}
export default HoverCounter;

While our code samples may be valid, there’s a significant issue: both files share similar code logic, violating the DRY (Don’t Repeat Yourself) principle. So how can we address this problem?

Enter Higher-Order Components (HOCs). These components allow React developers to reuse code logic across their projects, resulting in less repetition and more optimized, readable code.

Higher-Order Components (HOCs) Use Cases

In React, HOCs provide a versatile way to enhance component functionality and behavior. They can be applied to various use cases within your application. Here’s a list of common scenarios where HOCs prove useful:

Conditional Rendering

Use HOCs to conditionally render components based on specific logic, such as user authentication or permission checks. For example:

function withConditionalRendering(WrappedComponent) {
  return function(props) {
    if (props.condition) {
      return <WrappedComponent {...props} />;
    } else {
      return null;
    }
  };
}

Authentication

Implement user authentication and authorization. An AuthHOC can protect routes or components, ensuring that only authenticated users have access. For example:

function withAuthentication(WrappedComponent) {
  return function(props) {
    if (props.isAuthenticated) {
      return <WrappedComponent {...props} />;
    } else {
      return <Redirect to="/login" />;
    }
  };
}

Data Fetching

Handle data fetching and loading states. An HOC can fetch data and pass it as props to the wrapped component, managing loading and error states. For example:

function withDataFetching(WrappedComponent) {
  return class extends React.Component {
    state = { data: null, isLoading: true, error: null };

    async componentDidMount() {
      try {
        const response = await fetch(this.props.url);
        const data = await response.json();
        this.setState({ data, isLoading: false });
      } catch (error) {
        this.setState({ error, isLoading: false });
      }
    }

    render() {
      return <WrappedComponent {...this.props} {...this.state} />;
    }
  };
}

Styling

Apply CSS styles or themes to components. An HOC can pass styling information as props, allowing customization of component appearance. For example:

function withStyling(WrappedComponent) {
  return function(props) {
    const style = { color: props.theme.primaryColor };
    return <WrappedComponent {...props} style={style} />;
  };
}

>> Read more: Tailwind CSS for React UI Components: Practical Code Examples

State Management

Share state (e.g., global app state or Redux store data) among multiple components using an HOC. For example:

function withStateManagement(WrappedComponent) {
  return function(props) {
    const [state, setState] = useState(props.initialState);
    return <WrappedComponent {...props} state={state} setState={setState} />;
  };
}

Logging and Analytics

Wrap components with an HOC to implement logging, error tracking, or analytics that report events or errors. For example:

function withLogging(WrappedComponent) {
  return function(props) {
    console.log(`Rendered ${WrappedComponent.name} with props:`, props);
    return <WrappedComponent {...props} />;
  };
}

Caching and Memoization

Improve performance by caching expensive computations or memoizing functions using an HOC. For example:

let cache = {};
function withMemoization(WrappedComponent) {
  return function(props) {
    const key = JSON.stringify(props);
    if (cache[key]) {
      return cache[key];
    } else {
      const result = <WrappedComponent {...props} />;
      cache[key] = result;
      return result;
    }
  };
}

Internationalization (i18n)

HOCs can facilitate translation and internationalization features for your components. By passing translated content or language preferences, you can create a more inclusive and globally accessible application. For example:

function withInternationalization(WrappedComponent) {
  return function(props) {
    const { locale, translations } = props;
    const translate = (key) => translations[locale][key] || key;
    return <WrappedComponent {...props} translate={translate} />;
  };
}

Flexibility of HOCs empowers you to adapt them to various use cases in your React application, resulting in more modular, reusable, and maintainable code.

The Architecture of A Higher-Order Component

A higher-order component operates as a function that accepts a component as an input and produces a fresh component as its output.

Using code, it can be like so:

// The enhancedComponent is created by applying the higherOrderFunction to the BaseComponent
const enhancedComponent = higherOrderFunction(BaseComponent);
  • enchanceComponent: This will be the component that has been augmented.
  • higherOrderFunction: As implied by its name, this function will augment the BaseComponent.
  • BaseComponent: This is the component we wish to augment or, in other words, enhance.

Here’s an in-depth structure of a higher-order component (HOC) in React:

Initially, you need to define a function. This function should be a JavaScript function that accepts the base component as a parameter and returns a new component with additional functionality. In a functional HOC, hooks can be utilized for state and side effects:

import React from 'react';

const withModification = (OriginalComponent) => {
  // High Order Component (HOC) logic with React hooks
  return function ModifiedComponent(properties) {
    // Logic specific to the HOC using React hooks
    return (
      <OriginalComponent {...properties} modifiedProp="newValue" />
    );
  };
};

Then, modify the component. Inside the ModifiedComponent function, you can use hooks to manage state and perform side effects. Hooks like useStateuseEffect, and useRef can be used to implement additional behavior:

const withModification = (OriginalComponent) => {
 return function ModifiedComponent(properties) {
   const [count, setCount] = React.useState(0);

   React.useEffect(() => {
		// put effect here
   }, [count]);

   return (
     <OriginalComponent {...properties} modifiedProp="newValue" />
   );
 };
};

The next step is using the HOC. To use your functional HOC, wrap a component by passing it as an argument to your HOC function. The result will be a new component with the modified functionality:

const ModifiedComponent = withModification(OriginalComponent);

Using the modified component. You can use ModifiedComponent in your application just like any other React component, with the added functionality from the HOC:

function App() {
 return (
   <div>
     <ModifiedComponent prop1="value1" prop2="value2" />
   </div>
 );
}

In the next segment of the article, we will see React’s HOC concept in action with the modified component.

Implementing Higher-Order Components

Setting Up Our Repository

We have to first create a blank React project. To start, begin with the following code:

# follow these command for setting up repository
npx create-react-app hoc-tutorial

cd hoc-tutorial

cd src

mkdir components

For this article, we will build three custom components to demonstrate HOC usage:

ClickInc.js: This component will render a button and a piece of text. When the user clicks on this button (an onClick event), the fontSize property of the text will increase.

HoverInc.js: This component will be similar to that of ClickInc.js. However, unlike the former, this component will listen to onMouseOver events.

In your project, navigate to the components folder. Here, create these two new files. Additionally, you will have App.jsindex.js, and style.css in the src directory, and package.json in the root directory. When that’s done, your file structure should look like this:

public
└── src
    └── components
	        └── ClickInc.js
					└── HoverInc.js
				└── App.js
				└── index.js
				└── style.css
		└── package.json

Having established the foundation for the project, we can now proceed to construct our components.

Constructing Our Components

Begin by scripting the following code in ClickInc.js:

//file name: components/ClickIncrement.js
function ClickIncr() {
  const [textSize, setTextSize] = useState(10); // Initialize Hook with a value of 10.
  return (
    <div>
      {/* On click, increase the value of textSize */}
      <button onClick={() => setTextSize((size) => size + 1)}>
        Increment on click
      </button>
      {/* Set the text size to the textSize variable and display its value. */}
      <p style={{ fontSize: textSize }}>Text size in onClick function: {textSize}</p>
    </div>
  );
}
export default ClickIncrement;

Next, copy the following lines of code into your HoverIncr component:

function HoverIncr(props) {
  const [textSize, setTextSize] = useState(10); // Initialize Hook with a value of 10.
  return (
    <div>
      {/* This time, we're listening to hover events instead of clicks */}
      <button onMouseOver={() => setTextSize((size) => size + 1)}>
        Increment on hover
      </button>
      {/* Set the text size to the textSize variable and display its value */}
      <p style={{ fontSize: textSize }}>
        Text size in onMouseOver function: {textSize}
      </p>
    </div>
  );
}
export default HoverIncrement;

Lastly, render these functions on the GUI as follows:

//import component
import ClickIncrement from "./components/ClickIncr";
import HoverIncrement from "./components/HoverIncr";
export default function App() {
  return (
    <div className="App">
      <ClickIncrement />
      <HoverIncrement />
    </div>
  );
}

Implementing Our HOC Function

Initiate by generating a file named withCounter.js inside the components directory. Subsequently, commence the coding process in this file.

import React from "react";

// Define our Higher Order Component (HOC)
const EnhancedComponent = (BaseComponent) => {
  function WrappedComponent(props) {
    // Render the BaseComponent and forward its props
    return <BaseComponent {...props} />;
  }
  // Return the new component
  return WrappedComponent;
};

export default EnhancedComponent;

Let’s break down this code step by step:

Initially, we formulated a function named EnhancedComponent that accepts an argument termed BaseComponent. In this context, the BaseComponent refers to the React element that will be encapsulated.

Next, we instructed React to project BaseComponent onto the UI. The enhancement functionality will be incorporated later in this article.

Upon completion, it’s time to employ the EnhanceComponent function in our application. To achieve this, navigate to the HoverIncr.js file and commence writing the subsequent lines:

import withCounter from "./withCounter.js" // Import the withCounter

function HoverIncr() {
//..custom code
}
// Update your 'export' statement to:
export default withCounter(HoverIncr);
// HoverIncr has now been enhanced by the withCounter HOC.

Proceed with the identical procedure for the ClickIncr module:

//file: components/ClickInc.js
import withCounter from "./withCounter";
function ClickIncrease() {
//...custom code
}
export default withCounter(ClickIncrease);
//wrapped component of the withCounter method.

Observe that our outcome remains the same. This is due to the fact that we have not yet modified our Higher-Order Component (HOC). In the following section, you’ll discover how to distribute props among our components.

Distribute Props

React, through the use of Higher-Order Components (HOCs), enables the sharing of properties among the components wrapped within the project.

For initiation, establish a name property in withCounter.js as follows:

// Filename: components/withCounter.js
const EnhancedComponent = (BaseComponent) => {
  function WrappedComponent(props) {
    // In this section, we introduce a 'name' prop
    return <BaseComponent name="Rellia" />;
  }
  // ... custom code ...
}

To access this data property, we simply need to implement the following modifications to its subordinate components:

// In components/HoverIncr
function HoverIncr(parameters) {   // Receive the shared parameters
  return (
    <div>
      {/* custom code..*/}
      {/* Render the value of the 'name' parameter in HoverIncr */ }
      <p> Value of 'name' in HoverIncreaseComponent: {parameters.name}</p>
    </div>
  );
}

// In components/ClickIncr.js
function ClickIncr(parameters) {
  // Handle incoming parameters
  return (
    <div>
      {/* custom code..*/}
      <p>Value of 'name' in ClickIncreaseComponent: {parameters.name}</p>
    </div>
  );
}

That was a breeze! It’s how easy that React’s HOC design enables developers to distribute data among components effortlessly.

In the following sections, you will discover how to disseminate states through HOC functions.

State Variables Management Using Hooks

In the same way as we share props, Hooks can also be shared through Higher-Order Components (HOCs):

//In components/withCounter.js
const EnhancedComponent = (BaseComponent) => {
  function WrappedComponent(props) {
    const [counter, setCounter] = useState(1);
    return (
      <BaseComponent
        counter={counter}
        incrementCounter={() => setCounter((counter) => counter + 1)}
      />
    );
  }
//custom code..

Here’s a rephrased version of the code explanation:

Initially, we establish a Hook variable named counter and assign it an initial value of 1. We also formulate a function called incrementCounter. This function, when called, will increase the counter value. We then export the incrementSize function and the size Hook as properties. Consequently, this grants the UpdatedComponent’s wrapped components access to these Hooks.

The final step involves utilizing the counter Hook. To accomplish this, insert the following lines of code into the HoverIncr and ClickIncr modules:

// Apply the following modifications to 
// components/HoverIncr.js and ClickIncr.js
const { count, increaseCount } = props;
return (
  <div>
    {/* Invoke the 'increaseCount' function to increment the 'count' state. */}
    <button onClick={() => increaseCount()}>Increase count</button>
    {/* Display the value of our 'count' variable. */}
    <p> This is the value of 'count' in HoverIncrease/ClickIncrease: {count}</p>
  </div>
);

It’s crucial to understand that the counter state’s value isn’t distributed among our child components. If you’re looking to share states across multiple React components, consider using React’s Context API. This tool enables you to share states and Hooks across your application with ease.

Passing Parameters

Consider this scenario: our code is functioning as expected, but what if we need to increase the counter value by a custom amount? Through the use of Higher-Order Components (HOCs), we can instruct React to deliver specific data to designated child components. This is achievable with the use of parameters.

To incorporate parameter support, insert the following code into components/withCounter.js:

//This function will now accept an 'increaseCount' parameter.
const EnhancedComponent = (BaseComponent, increaseCount) => {
  function WrappedComponent(props) {
    return (
      <BaseComponent
				// Now, we increase the 'counterValue' variable by 'incrementValue'
        incrementCounter={() => setCounter((size) => size + increaseCount)}
      />
    );
//further code..

In the given code, we have updated our function to accept an extra argument named increaseCount.

The only thing remaining is to utilize this argument in our wrapped components. To achieve this, insert the following line of code in both HoverIncr.js and ClickIncr.js:

//In HoverIncr, change the 'export' statement:
export default withCounter(HoverIncr, 10); //value of increaseCount is 10.
//this will increment the 'counter' Hook by 10.
//In ClickIncrease:
export default withCounter(ClickIncr, 3); //value of increaseCount is 3.
//will increment the 'counter' state by 3 steps.

Finally, your withCounter.js file should appear as follows:

import React from "react";
import { useState } from "react";
const EnhancedComponent = (BaseComponent, increaseCount) => {
  function WrappedComponent(props) {
    const [counter, setCounter] = useState(10);
    return (
      <BaseComponent
        name="LogRocket"
        counter={counter}
        incrementCounter={() => setCounter((size) => size + increaseCount)}
      />
    );
  }
  return WrappedComponent;
};
export default EnhancedComponent;

and HoverIncr.js be like this:

import { useState } from "react";
import withCounter from "./withCounter";
function HoverIncr(props) {
  const [fontSize, setFontSize] = useState(10);
  const { counter, incrementCounter } = props;
  return (
    <div>
      <button onMouseOver={() => setFontSize((size) => size + 1)}>
        Increase on hover
      </button>
      <p style={{ fontSize }}>
       Font size in onMouseOver function: {fontSize}
      </p>
      <p> Value of 'name' in HoverIncr: {props.name}</p>
      <button onClick={() => incrementCounter()}>Increment counter</button>
      <p> Value of 'counter' in HoverIncr: {counter}</p>
    </div>
  );
}
export default withCounter(HoverIncrease, 10);

To wrap things up, the ClickIncr component should contain the following code:

import { useEffect, useState } from "react";
import withCounter from "./withCounter";
function ClickIncr(props) {
  const { counter, incrementCounter } = props;
  const [fontSize, setFontSize] = useState(10);
  return (
    <div>
      <button onClick={() => setFontSize((size) => size + 1)}>
        Increase with click
      </button>
      <p style={{ fontSize }}>Size of font in onClick function: {fontSize}</p>
      <p>Value of 'name' in ClickIncr: {props.name}</p>
      <button onClick={() => incrementCounter()}>Increment counter</button>
      <p> Value of 'counter' in ClickIncr: {counter}</p>
    </div>
  );
}
export default withCounter(ClickIncrease, 3);

Typical Issue with HOCs: Propagation of Props to Certain Components

It’s crucial to understand that the method of propagating props to a child component of an HOC differs from that of a component that is not an HOC.

Consider the code snippet below for instance:

function App() {
  return (
    <div>
      {/*Provide a 'hiddenWord' prop*/}
      <HoverIncr hiddenWord={"relia"} />
    </div>
  );
}

function HoverIncr(props) {
  //Extract prop value:
  console.log("Value of hiddenWord: " + props.hiddenWord);
  //Additional code goes here..
}

Ideally, the console should display the message Value of hiddenWord: relia. However, we encounter an issue where it returns undefined.

In this scenario, the hiddenWord prop is being directed to the withCounter function, rather than the HoverIncr component.

To rectify this problem, a minor modification is required in withCounter.js:

const EnhanceComponent = (BaseComponent, increaseCount) => {
  function WrappedComponent(props) {
    return (
      <BaseComponent
        {...props} // pasing down all props
      />
    );
  }
  return WrappedComponent;
};

Pros and Cons of React Higher-Order Components (HOCs)

Here's a breakdown of the advantages and disadvantages of HOCs in React:

Pros:

  • Code Reusability: HOCs promote code reuse by encapsulating common functionalities like authentication, data fetching, or styling into a single component. This reduces code duplication and improves maintainability across your application.
  • Improved Readability and Maintainability: By separating concerns, HOCs keep components focused on their core functionality and UI logic. This makes the codebase easier to read and maintain, especially for larger projects with many components. 
  • Separation of Concerns: HOCs encourage a modular approach to development. Each HOC handles a specific task, making the code more organized and easier to reason about. This improves testability and reduces the risk of bugs.
  • Easier Testing: Since HOCs isolate functionalities, it's often easier to test them in isolation compared to testing components with tightly coupled logic. This allows for more efficient and targeted unit testing.

Cons:

  • Increased Complexity: While promoting modularity, HOCs can add an extra layer of complexity to the codebase, especially when used extensively or nested deeply. This can make it challenging to understand the flow of data and logic within the component hierarchy.
  • Debugging Challenges: Debugging issues within HOCs can be difficult due to the wrapped component's behavior being intertwined with the HOC's logic. Tracing the source of errors can be time-consuming, especially with nested HOCs.
  • Potential for Unintended Side Effects: HOCs can introduce unintended side effects if not carefully designed. Modifying props or state in unexpected ways can lead to unexpected behavior in wrapped components, requiring extra vigilance during development.

Best Practices for Using React Higher-Order Components (HOCs)

Here are some guidelines for effectively using HOCs in your React development projects:

When to Use HOCs?

  • Code Reusability: When you have a functionality (like authentication, data fetching, or styling) used across multiple components, an HOC can encapsulate that logic, promoting code reuse and reducing redundancy.
  • Separation of Concerns: If a component's logic becomes cluttered with non-UI related concerns, an HOC can isolate those concerns, improving readability and maintainability.
  • Enforcing Consistency: HOCs can enforce consistent behavior or styling across a group of components, ensuring a unified user experience.

When to Avoid HOCs?

  • Simple Logic: If the logic you want to reuse is very simple, directly implementing it within the component might be more efficient and easier to understand.
  • Over-engineering: Don't create an HOC for every small functionality. Evaluate if the complexity of the HOC outweighs the benefit of reusability.
  • Debugging Challenges: Nested HOCs can make debugging intricate. Use them judiciously to avoid creating a tangled mess.

Naming Conventions for HOCs

  • Use clear and descriptive names that reflect the HOC's purpose. Examples: withAuthenticationwithDataFetchingwithErrorBoundary.
  • Consider prefixes like with or enhance to indicate it's an HOC.
  • Maintain consistency with your project's naming conventions.

Testing HOCs

  • Unit Testing: Test HOCs in isolation to ensure they modify the wrapped component's behavior as expected. Use mocking libraries to simulate dependencies like data fetching.
  • Integration Testing: Test how the HOC interacts with the wrapped component and other parts of your application.

Composing Multiple HOCs

HOCs can be composed together to create more complex functionalities. However, be mindful of potential issues:

  • Prop Drilling: When multiple HOCs pass down props, it can lead to "prop drilling," making components difficult to understand. Consider context API or render props to manage props effectively.
  • Order of Execution: The order in which HOCs are composed can sometimes affect behavior. Consider using utility libraries like recompose to manage the order of HOC application.

By following these guidelines, you can leverage the power of HOCs to create reusable, maintainable, and well-tested React components. Remember, HOCs are a tool, and like any tool, they should be used strategically to enhance your codebase, not overcomplicate it.

>> Read more:

Final Thoughts

Through this article, you’ve gained a solid understanding of the core principles of React HOCs. If you faced any challenges while reading this article, I recommend breaking down and experimenting with the provided code examples. This will enhance your comprehension of higher-order components.

>>> Follow and Contact Relia Software for more information!

  • coding