Rethinking React: The Moment I Recalled the Power of HOCs

react design pattern higher order component

In this blog post, I’ll share how I ended up using a Higher-Order Component (HOC) to implement Role-Based Access Control, why this pattern has been significant for me, and—if you’re interested—I’ll also walk you through some potential use cases for HOCs and how the pattern works.

Table of Contents

I was implementing Role-Based Access Control in our user interface, with the goal of preventing users from accessing certain functionalities by not rendering elements they don’t have permission to use.

So I created this custom hook

const { hasPermission } = useRBAC()

Previously, my approach was to open each component individually and add the permission logic manually.

// AnalyzeButton.tsx

import React from 'react';
import { useRBAC } from 'path-to-my-hooks'; 

const AnalyzeButton => {
  const { hasPermission } = useRBAC();

  if (!hasPermission('TOOL_NAME_ANALYZE_SOMETHING')) return null;

  return (
    <button>
      Analyze
    </button>
  );
};

export default AnalyzeButton;

It was easy to do at first—I added the permission logic manually to over 15 different components. But when I started writing unit tests, that’s when the process began to slow me down.

I realized I needed a better approach. That’s when I remembered a course I took on Udemy a while back, where they introduced the concept of Higher-Order Components (HOCs). Back then, it didn’t really make sense to me, and I wasn’t sure how to apply it in a real project. But now, being in the situation, it finally clicked.

The key takeaway here is that reading books and watching tutorials helps build a foundation of knowledge and awareness. If I hadn’t known about HOCs, I probably would’ve stayed stuck doing things the tedious way.

So in this post, I’ll share how I used a Higher-Order Component to streamline my workflow—and how it ended up saving me both time and effort. Here’s what I learned.

Write once, test once—keep existing components unchanged.

What I really like about using a Higher-Order Component (HOC) is that you only need to write the logic and its unit test once.

Imagine you’re tasked with updating 15+ components just to hide them under certain conditions—like when a user lacks permission (e.g., RBAC). It sounds simple:

Initial Estimation Example

  • Effort: 2 story points
  • Complexity: Low (just a basic if-else)
  • Volume: High (15+ components)

Even though the logic is simple, doing the same change across many components adds up. Then, consider that you’re required to write unit tests for each one. In most cases, those tests are harder to write than the feature itself. The new total might look like this:

Revised Estimation Example

  • Effort: 5 story points
  • Complexity: Still Low
  • Volume:
    • 15+ component changes
    • 15+ unit tests (each covering visibility scenarios)
    • Possible refactoring of existing components and their tests

By using the HOC pattern instead:

  • You write the conditional logic once
  • You write a unit test for it once
  • You don’t touch existing components
  • You reuse the HOC wherever it’s needed

This approach cuts down 90% of the work and avoids repetitive, error-prone edits. More importantly, it keeps your codebase cleaner and easier to maintain.

The “naive” approach—adding logic directly to every component—creates unnecessary effort and increases the risk of inconsistencies over time.

Now, let’s take a look at how the HOC pattern works and explore where else it can be applied.

Higher-Order Components Explained

An HOC is just a component that accepts component and returns new component with added functionality.

An HOC is like putting on a jacket. The jacket (HOC) adds new behavior (keeps you warm, adds style), but you remain the same person (component) underneath

Here are some practical examples where HOCs can be used:

1. Permission Control (RBAC)

Wrap any component to only render if the user has the right permission.

const withPermission = (WrappedComponent, requiredPermission) => {
  return (props) => {
    const { hasPermission } = useRBAC();
    if (!hasPermission(requiredPermission)) {
      return <div>🚫 You don’t have access</div>;
    }
    return <WrappedComponent {...props} />;
  };
};

2. Loading State Wrapper

Add a loader until data is fetched, reusable across tables, charts, or forms.

const withLoading = (WrappedComponent) => {
  return ({ isLoading, ...props }) => {
    if (isLoading) return <div>Loading...</div>;
    return <WrappedComponent {...props} />;
  };
};

3. Form Validation

Wrap inputs or forms with validation logic (e.g., required fields, regex).

const withValidation = (WrappedComponent, validateFn) => {
  return (props) => {
    const [error, setError] = React.useState("");
    const handleChange = (e) => {
      const value = e.target.value;
      const errorMsg = validateFn(value);
      setError(errorMsg);
    };
    return (
      <div>
        <WrappedComponent {...props} onChange={handleChange} />
        {error && <span style={{ color: "red" }}>{error}</span>}
      </div>
    );
  };
};

4. Theming / Styling

Apply consistent UI styling or themes across components.

const withTheme = (WrappedComponent, themeClass) => {
  return (props) => (
    <div className={themeClass}>
      <WrappedComponent {...props} />
    </div>
  );
};

5. Analytics / Logging

Track interactions without cluttering components.

const withLogging = (WrappedComponent, eventName) => {
  return (props) => {
    const handleClick = () => {
      console.log(`Event: ${eventName}`);
      if (props.onClick) props.onClick();
    };
    return <WrappedComponent {...props} onClick={handleClick} />;
  };
};

6. Authentication Redirect

Redirect users to login if they’re not authenticated.

const withAuth = (WrappedComponent) => {
  return (props) => {
    const { isAuthenticated } = useAuth();
    return isAuthenticated ? (
      <WrappedComponent {...props} />
    ) : (
      <Navigate to="/login" />
    );
  };
};

Higher-Order Pattern

Here is the pattern of HOC in Javascript

const withSomething = (WrappedComponent) => {
  return (props) => {
    // Add logic, props, or side effects here
    return <WrappedComponent {...props} additionalProp="value" />;
  };
};

  1. Takes a component
    • It accepts a component as an argument. Often called WrappedComponent
  2. Return a new component
    • It returns a new functional component that usually renders the original component with added props, state, or behavior.
  3. Props Forwarding
    • It typically forwards the original props via {...props} so the wrapped component still receives them.
  4. Enhancement Logic
    • You can inject:
      • Conditional rendering
      • State or side-effects
      • Additional props
      • Styling

Example: withLoading HOC

The HOC

const withLoading = (WrappedComponent) => {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <p>Loading...</p>;
    return <WrappedComponent {...props} />;
  };
};

A Simple Component to Wrap

// UserList.js
import React from 'react';

const UserList = ({ users }) => (
  <ul>
    {users.map((user, index) => (
      <li key={index}>{user}</li>
    ))}
  </ul>
);

export default UserList;

Use the HOC

import React from 'react';
import withLoading from './withLoading';
import UserList from './UserList';

const UserListWithLoading = withLoading(UserList);

const App = () => {
  const isLoading = true;
  const users = ['Alice', 'Bob', 'Charlie'];

  return <UserListWithLoading isLoading={isLoading} users={users} />;
};

export default App;

Two Ways You Can Build a Higher-Order Component

Exporting HOC

This approach keeps things clean: wrap Dashboard with the HOC a single time, and you’re free to reuse it everywhere without the extra hassle.

function Dashboard({ user }) {
  return <h1>Welcome, {user}</h1>;
}

// Export wrapped version
export default withTitle(Dashboard);

Outside Component Render Method

Another way is to apply the HOC outside the component’s render method. This gives you more flexibility—you can wrap Dashboard with the withTitle HOC, or combine it with other HOCs when needed. It doesn’t lock you into using just one HOC.

const DashboardWithTitle = withTitle(Dashboard);

function App() {
  return <DashboardWithTitle user="Jiyo" />;
}

Where Most People Go Wrong with HOCs

A Common Mistake with HOCs If you’re just getting started with Higher-Order Components, you might accidentally define the HOC inside your component, like this:

function App() {
  const DashboardWithTitle = withTitle(Dashboard); // creates a new component on every render
  return <DashboardWithTitle user="Jiyo" />;
}

At first glance, this looks fine. But the problem is that React treats DashboardWithTitle as a brand-new component every time App re-renders.

Why is this bad?

  • Every new wrapped component gets a new reference in memory.
  • React sees it as “different” from the previous one, so it unmounts the old component and mounts a fresh one.
  • This cycle keeps repeating, leading to unnecessary remounts.

What are the side effects?

  • Your app slows down due to repeated mounting/unmounting.
  • Component state is lost (it never persists between renders).
  • Network calls inside the component may fire multiple times, causing duplicate requests and even 4XX errors.
  • Data tables take too long to load since they keep re-fetching and re-rendering unnecessarily.

The Fix

Always define your HOC outside of the component render, using the approaches we discussed earlier. This ensures the wrapped component keeps a stable reference, React can optimize rendering, and your state/data remain intact.

Final Thoughts

What started as a straightforward task—adding permission checks to a handful of components—quickly became repetitive and inefficient. Manually editing over 15 components and writing tests for each one was not only time-consuming, but it also introduced unnecessary complexity to the codebase.

Discovering (or rather, recalling) the HOC pattern helped me step back and rethink the problem. Instead of scattering logic throughout the app, I was able to centralize it, reuse it, and make the code more maintainable. It’s a great reminder that many problems we face in development have already been solved—we just need to recognize the pattern and apply it when the time is right.

If you’re finding yourself copy-pasting similar logic across components, it might be a sign that a cross-cutting concern is at play. Consider abstracting it into a reusable pattern like a HOC. It could save you hours—not just now, but in future maintenance and testing.

Have you faced similar challenges with RBAC or other cross-cutting concerns? I’d love to hear how you approached them.

code with jiyo logo

Subscribe to Newsletter

Get my latest blog posts—packed with lessons I’ve learned along the way that have helped me become a faster, smarter web developer.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top