Mastering React HOCs: A Clear Guide to Reusable Code

Tanzim HossainTanzim Hossain
10 min read

I’ve been diving into React patterns, jotting notes in Notion, and Higher-Order Components (HOCs) have been a game-changer. 🚀 They help you reuse code and keep your app tidy. In this post, I’ll explain what HOCs are, share simple examples, and show real-world uses—all easy to follow. Whether you’re new to React or sharpening your skills, you’ll see why HOCs are worth learning. Let’s get started! 🎉


What’s an HOC in React? 🤔

A Higher-Order Component (HOC) is a function that takes a component and returns a new one with extra features. Think of it as upgrading a plain bike to an electric one—it’s still a bike, but now it does more. 🚴‍♂️⚡

Here’s how it works:

  • Start with a component, like a button or form. 🔘

  • The HOC wraps it, adding things like data fetching or user checks. 🔄

  • You get a new component that’s more powerful without changing the original. 💪

HOCs are great because they:

  • Save you from repeating code. 🔄

  • Keep your components clean and focused. 🧼

  • Share logic across your app. 🔗

Let’s check out a basic HOC to see it in action. 👀


Example 1: Debugging Props with an HOC 🐞

When I started React, I had trouble tracking props. So, I built an HOC to log them to the console. It’s a simple way to debug without messing up your components. 🛠️

Code: checkProps.js

// HOC to log props for debugging
export const checkProps = (Component) => {
  // Returns a new component
  return (props) => {
    // Show props in console
    console.log("Props received:", props);
    // Pass all props to original component
    return <Component {...props} />;
  };
};

🧱 Usage: App.js

import { checkProps } from "./checkProps"; // Import HOC
import { UserInfo } from "./UserInfo"; // Import component

// Wrap UserInfo with HOC
const UserInfoWrapper = checkProps(UserInfo);

function App() {
  return (
    <div>
      // Use wrapped component with props
      <UserInfoWrapper name="John" age={23} />
    </div>
  );
}

export default App;

📃 Component: UserInfo.js

// Simple component to show user info
export const UserInfo = ({ name, age }) => (
  <div>
    <h2>{name}</h2>
    <p>Age: {age}</p>
  </div>
);

How It Works

  1. checkProps takes a component (like UserInfo).

  2. It returns a new component that:

    • Logs props (e.g., { name: "John", age: 23 }) to the console.

    • Passes all props to UserInfo using {...props}.

  3. In App, we use UserInfoWrapper instead of UserInfo.

  4. Open your console, and you’ll see the props printed.

Why It’s Useful: No more adding console.log everywhere. This HOC lets you debug props for any component, keeping your code clean. 🧹

Note:I used to typo prop names and waste time. This HOC helped me spot mistakes fast. Try it for quick debugging! ⏱️


Example 2: Fetching Data with an HOC 📡

HOCs are perfect for handling data, like pulling user info from a server. This example shows an HOC that fetches a user’s details and lets you edit them in a form. It’s practical and reusable. 🔄

Code: includeUpdatableUser.js

import { useEffect, useState } from "react";
import axios from "axios"; // For server requests

// HOC to fetch and edit user data
export const includeUpdatableUser = (Component, userId) => {
  return (props) => {
    // Store original user data
    const [user, setUser] = useState(null);
    // Store editable user data
    const [updatableUser, setUpdatableUser] = useState(null);

    // Fetch data when component loads
    useEffect(() => {
      (async () => {
        const response = await axios.get(`/users/${userId}`);
        setUser(response.data); // Save original
        setUpdatableUser(response.data); // Save editable
      })();
    }, []); // Run once

    // Update data when user types
    const userChangeHandler = (updates) => {
      setUpdatableUser({ ...updatableUser, ...updates });
    };

    // Save changes to server
    const userPostHandler = async () => {
      const response = await axios.post(`/users/${userId}`, {
        user: updatableUser,
      });
      setUser(response.data); // Update original
      setUpdatableUser(response.data); // Update editable
    };

    // Reset to original data
    const resetUserHandler = () => {
      setUpdatableUser(user);
    };

    // Pass data and functions to component
    return (
      <Component
        {...props}
        updatableUser={updatableUser}
        changeHandler={userChangeHandler}
        userPostHandler={userPostHandler}
        resetUserHandler={resetUserHandler}
      />
    );
  };
};

📃 Usage: UserInfoForm.js

import { includeUpdatableUser } from "./includeUpdatableUser";

// Wrap form with HOC
export const UserInfoForm = includeUpdatableUser(
  ({ updatableUser, changeHandler, userPostHandler, resetUserHandler }) => {
    // Get name and age, or empty if null
    const { name, age } = updatableUser || {};

    // Show form or loading
    return updatableUser ? (
      <div>
        <label>
          Name:
          <input
            value={name}
            onChange={(e) => changeHandler({ name: e.target.value })}
          />
        </label>
        <br />
        <label>
          Age:
          <input
            value={age}
            onChange={(e) => changeHandler({ age: Number(e.target.value) })}
          />
        </label>
        <br />
        <button onClick={resetUserHandler}>Reset</button>
        <button onClick={userPostHandler}>Save</button>
      </div>
    ) : (
      <h3>Loading...</h3>
    );
  },
  "3" // User ID
);

function App() {
  return <UserInfoForm />;
}

export default App;

How It Works

  1. includeUpdatableUser takes a component and userId.

  2. It fetches data (e.g., { name: "John", age: 23 }) from /users/${userId}.

  3. It uses two states:

    • user: Original data.

    • updatableUser: Editable copy.

  4. It passes four props:

    • updatableUser: Data to show.

    • changeHandler: Updates data on input.

    • userPostHandler: Saves to server.

    • resetUserHandler: Restores original data.

  5. UserInfoForm shows a form to edit name and age. Users can save or reset.

Why It’s Useful: This HOC handles all data tasks—fetching, editing, saving—so your form stays focused. Reuse it for any user by swapping the userId.

Note: My app crashed without a loading state. Adding <h3>Loading...</h3> fixed it. Always cover loading cases! ⏳


Example 3: A Reusable HOC for Any Resource 🔄

The user HOC was neat, but I wanted one for any data—products, posts, anything. This generic HOC is flexible and cuts down on repetitive code. ✂️

Code: includeUpdatableResource.js

import { useEffect, useState } from "react";
import axios from "axios";

// Capitalize names (e.g., "product" -> "Product")
const toCapital = (str) => str.charAt(0).toUpperCase() + str.slice(1);

// HOC for any resource
export const includeUpdatableResource = (Component, resourceUrl, resourceName) => {
  return (props) => {
    // Original data
    const [data, setData] = useState(null);
    // Editable data
    const [updatableData, setUpdatableData] = useState(null);

    // Fetch data on load
    useEffect(() => {
      (async () => {
        const response = await axios.get(resourceUrl);
        setData(response.data);
        setUpdatableData(response.data);
      })();
    }, []); // Run once

    // Update editable data
    const changeHandler = (updates) => {
      setUpdatableData({ ...updatableData, ...updates });
    };

    // Save to server
    const dataPostHandler = async () => {
      const response = await axios.post(resourceUrl, {
        [resourceName]: updatableData,
      });
      setData(response.data);
      setUpdatableData(response.data);
    };

    // Reset to original
    const resetHandler = () => {
      setUpdatableData(data);
    };

    // Dynamic props (e.g., product, onChangeProduct)
    const resourceProps = {
      [resourceName]: updatableData,
      [`onChange${toCapital(resourceName)}`]: changeHandler,
      [`onSave${toCapital(resourceName)}`]: dataPostHandler,
      [`onReset${toCapital(resourceName)}`]: resetHandler,
    };

    // Pass props to component
    return <Component {...props} {...resourceProps} />;
  };
};

Usage: ProductForm.js

import { includeUpdatableResource } from "./includeUpdatableResource";

// Wrap product form with HOC
export const ProductForm = includeUpdatableResource(
  ({ product, onChangeProduct, onSaveProduct, onResetProduct }) => {
    const { name, price } = product || {};

    return product ? (
      <div>
        <label>
          Product Name:
          <input
            value={name}
            onChange={(e) => onChangeProduct({ name: e.target.value })}
          />
        </label>
        <br />
        <label>
          Price:
          <input
            value={price}
            onChange={(e) => onChangeProduct({ price: Number(e.target.value) })}
          />
        </label>
        <br />
        <button onClick={onResetProduct}>Reset</button>
        <button onClick={onSaveProduct}>Save</button>
      </div>
    ) : (
      <h3>Loading...</h3>
    );
  },
  "/products/1", // Product URL
  "product" // Resource name
);

function App() {
  return <ProductForm />;
}

export default App;

How It Works

  1. includeUpdatableResource takes:

    • Component: What to wrap.

    • resourceUrl: Data source (e.g., /products/1).

    • resourceName: Name like product.

  2. It fetches data and stores it in data (original) and updatableData (editable).

  3. It creates props like product, onChangeProduct, onSaveProduct, onResetProduct.

  4. ProductForm uses them to edit a product’s name and price.

Why It’s Useful: This HOC works for any data—users, products, posts. Just change the URL and name. It’s a one-size-fits-all tool for fetching and editing. 🛠️

Note: Dynamic props with toCapital made this HOC feel polished. Test it with different resources to see its power! 💪


Real-World HOC Use Cases 🌍

HOCs do more than fetch data. Here are three common ways they’re used in real apps, with examples to inspire you. 💡

  1. Securing Pages with Authentication 🔒

  2. Tracking Page Visits 📈

  3. Adding Themes to Components 🎨

Want to restrict a page to logged-in users or admins? An HOC can handle that.

Code: withAuth.js

import { useAuth } from "./useAuth"; // Get user info
import Redirect from "./Redirect"; // Redirect component
import AccessDenied from "./AccessDenied"; // Error component

// HOC to check login and role
export const withAuth = (requiredRole) => (Component) => (props) => {
  const { user } = useAuth();

  // No user? Go to login
  if (!user) {
    return <Redirect to="/login" />;
  }

  // Wrong role? Show error
  if (requiredRole && user.role !== requiredRole) {
    return <AccessDenied />;
  }

  // All clear? Show component
  return <Component {...props} user={user} />;
};

Usage

import { withAuth } from "./withAuth";

// Admin-only dashboard
const AdminDashboard = withAuth("admin")(({ user }) => (
  <div>
    <h2>Welcome, {user.name}!</h2>
    <p>Admin Dashboard</p>
  </div>
));

function App() {
  return <AdminDashboard />;
}

export default App;

Why It’s Useful: This HOC protects pages without cluttering your components with login checks. Reuse it for any restricted page.


2. Tracking Page Visits

Need to track when users visit a page? An HOC can log it automatically.

Code: withTracking.js

import { useEffect } from "react";
import analytics from "./analytics"; // Fake analytics tool

// HOC to track page views
export const withTracking = (eventName) => (Component) => (props) => {
  // Track load/unload
  useEffect(() => {
    analytics.trackPageView(eventName); // Log visit
    return () => {
      analytics.trackPageExit(eventName); // Log exit
    };
  }, []); // Run once

  // Show component
  return <Component {...props} />;
};

Usage

import { withTracking } from "./withTracking";

// Track checkout page
const TrackedCheckout = withTracking("checkout_page")(() => (
  <div>
    <h2>Checkout</h2>
    <input placeholder="Card number" />
    <button>Pay</button>
  </div>
));

function App() {
  return <TrackedCheckout />;
}

export default App;

Why It’s Useful: This HOC adds tracking without touching your component’s code. It’s great for understanding what users do.


3. Adding Themes to Components

Want light or dark mode in your app? An HOC can pass theme styles to components.

Code: withTheme.js

import { useTheme } from "./useTheme"; // Get theme info

// HOC to add theme
export const withTheme = (Component) => (props) => {
  const theme = useTheme(); // E.g., { color: "black" }

  // Pass theme to component
  return <Component {...props} theme={theme} />;
};

Usage

import { withTheme } from "./withTheme";

// Themed button
const ThemedButton = withTheme(({ theme }) => (
  <button style={{ background: theme.color, color: "white" }}>
    Click Me
  </button>
));

function App() {
  return <ThemedButton />;
}

export default App;

Why It’s Useful: This HOC keeps components styled consistently. Use it for buttons, cards, or anything that needs a theme.


Why Use HOCs? 🤔

HOCs solve real problems and make coding easier. Here’s why I love them:

  • Separation of concerns: Keep logic (like fetching) separate from UI (like forms). 🧩

  • Code reuse: Write logic once, use it in many components. 🔄

  • Testability: Test HOC logic alone, not mixed with UI. 🧪

  • Clean components: Avoid stuffing components with hooks or side effects. 🧼

Note: HOCs made my code feel organized, like sorting my desk. They’re worth the effort! 🗂️


When to Use HOCs vs. Other Patterns? 🤔

HOCs aren’t always the answer. Here’s a quick guide to pick the right tool:

  • Reuse logic across many components: HOC or custom hook 🔄

  • Need lifecycle logic + rendering: HOC 🔄

  • Small logic reuse (state, effect): Custom hook 🔄

  • Full control inside JSX: Avoid HOC here 🚫

Note: Modern React leans toward custom hooks for simple cases. But HOCs are still great for wrapping behavior, like logging, auth, analytics, or conditional rendering.

Note: I tried hooks for everything at first, but HOCs were better for big logic like auth. Know both to choose wisely! 🧠


inal Tip: Make HOCs Composable 🧩

HOCs are like building blocks—you can stack them to add more features. 🏗️

Example:

import { withAuth } from "./withAuth";
import { withTracking } from "./withTracking";

const MyComponent = () => <div>My App</div>;

// Chain HOCs
export default withAuth("admin")(withTracking("home_page")(MyComponent));

Or use a library like redux for cleaner chaining:

import { compose } from "redux";

export default compose(withAuth("admin"), withTracking("home_page"))(MyComponent);

Why It’s Useful: Composability lets you mix and match HOCs, like adding auth and tracking to one component. 🔄

Note: Chaining HOCs was tricky at first, but it’s super powerful. Start with two and build up! 🚀


Let’s Recap 📝

HOCs are a clever way to reuse logic in React. They wrap components to add features like debugging, data fetching, or security, keeping your code clean. My Notion notes taught me they’re not hard—just smart tools for better apps. 💡

0
Subscribe to my newsletter

Read articles from Tanzim Hossain directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tanzim Hossain
Tanzim Hossain