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" />;
};
};
- Takes a component
- It accepts a component as an argument. Often called
WrappedComponent
- It accepts a component as an argument. Often called
- Return a new component
- It returns a new functional component that usually renders the original component with added props, state, or behavior.
- Props Forwarding
- It typically forwards the original props via
{...props}so the wrapped component still receives them.
- It typically forwards the original props via
- Enhancement Logic
- You can inject:
- Conditional rendering
- State or side-effects
- Additional props
- Styling
- You can inject:
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.

