DEV Community

Cover image for Mastering React HOCs: A Clear Guide to Reusable Code 🚀
Tanzim Hossain
Tanzim Hossain

Posted on • Edited on

Mastering React HOCs: A Clear Guide to Reusable Code 🚀

I’ve been diving deep into React patterns, organizing my thoughts in Notion, and Higher-Order Components (HOCs) have truly been a game-changer! 🌟 They help you reuse code and keep your app clean and organized. In this post, I’ll break down what HOCs are, share simple examples, and explore real-world use cases—all in an easy-to-follow way. Whether you’re new to React or leveling up your skills, you’ll see why HOCs are worth learning. Let’s dive in! 🎉

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 the basic idea:

  • Start with a component, like a button or form. 🔘
  • The HOC wraps it, adding features like data fetching or user authentication. 🔄
  • You get a new component that’s more powerful without modifying the original. 💪

Why Use HOCs?

  • Save Time: Avoid repeating code across components. 🔄
  • Stay Clean: Keep your components focused and tidy. 🧼
  • Share Logic: Reuse the same logic across your app. 🔗

Let’s see an HOC in action with some practical examples. 👀

Example 1: Debugging Props with an HOC 🐞

When I started with React, I struggled to track props, often wasting time on typos. So, I built an HOC to log props to the console—a simple way to debug without cluttering my 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 the original component
    return <Component {...props} />;
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Component: UserInfo.js

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

How It Works

  1. checkProps takes a component (like UserInfo).
  2. It returns a new component that:
    • Logs the props (e.g., { name: "John", age: 23 }) to the console.
    • Passes all props to UserInfo using {...props}.
  3. In App.js, 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. 🧹 I used to typo prop names all the time—this HOC helped me spot mistakes fast. Try it for quick debugging! ⏱️

Example 2: Fetching Data with an HOC 📡

HOCs are awesome for handling data, like fetching user info from a server. This example shows an HOC that pulls user details and lets you edit them in a form—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}
      />
    );
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 to the component:
    • updatableUser: Data to display.
    • changeHandler: Updates data on input.
    • userPostHandler: Saves changes to the server.
    • resetUserHandler: Restores the original data.
  5. UserInfoForm renders a form to edit the user’s name and age, with buttons to save or reset.

Why It’s Useful

This HOC handles all data tasks—fetching, editing, saving—so your form stays focused on the UI. You can reuse it for any user by swapping the userId. My app crashed once without a loading state, but adding <h3>Loading...</h3> fixed it. Always cover loading cases! ⏳

Example 3: A Reusable HOC for Any Resource 🔄

The user HOC was cool, but I wanted something more flexible—for products, posts, or any data. This generic HOC cuts down on repetitive code and works for any resource! ✂️

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} />;
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

How It Works

  1. includeUpdatableResource takes:
    • Component: The component 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 dynamic props like product, onChangeProduct, onSaveProduct, and onResetProduct.
  4. ProductForm uses these props to edit a product’s name and price.

Why It’s Useful

This HOC works for any data—users, products, posts, you name it! Just change the URL and resource name. It’s a one-size-fits-all tool for fetching and editing. 🛠️ The dynamic props with toCapital make it feel polished. Test it with different resources to see its power! 💪

Real-World HOC Use Cases 🌍

HOCs aren’t just for fetching data—they solve all kinds of real-world problems. Here are three common use cases with examples to inspire you! 💡

1. Securing Pages with Authentication 🔒

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

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Why It’s Useful

This HOC adds tracking without touching your component’s code. It’s perfect for understanding user behavior!

3. Adding Themes to Components 🎨

Want to add light or dark mode to 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} />;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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: Use an HOC or custom hook. 🔄
  • Need lifecycle logic + rendering: HOC is the way to go. 🔄
  • Small logic reuse (state, effect): Use a custom hook. 🔄
  • Full control inside JSX: Avoid HOCs here. 🚫

Modern React often leans toward custom hooks for simple cases, but HOCs shine for wrapping behavior—like logging, auth, analytics, or conditional rendering. I tried hooks for everything at first, but HOCs were better for big logic like auth. Know both to choose wisely! 🧠

Final 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));
Enter fullscreen mode Exit fullscreen mode

Or use a library like Redux for cleaner chaining:

import { compose } from "redux";

export default compose(withAuth("admin"), withTracking("home_page"))(MyComponent);
Enter fullscreen mode Exit fullscreen mode

Why It’s Useful

Composability lets you mix and match HOCs, like adding auth and tracking to one component. 🔄 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 and organized. My Notion notes taught me they’re not hard—just smart tools for building better apps. 💡

What’s your favorite React pattern? Let me know in the comments—I’d love to hear your thoughts! 💬

Top comments (2)

Collapse
 
nevodavid profile image
Nevo David

been messing with stuff like this too and its kinda underrated how much cleaner your apps get when you commit to good patterns over time. you think people stick with hocs long-term or move on to hooks mostly?

Collapse
 
chi4n profile image
Gian Franco Baeza

Nice, post