Modern frontend applications often need to support multiple user roles, such as admins, merchants, support agents etc. with each role having different permissions. Hardcoding these checks all over your components quickly becomes messy and error-prone.
In this guide, I’ll walk you through a real-world approach I used at work to elegantly manage role-based permissions in a React + Redux app using a Higher-Order Component (HOC) pattern.
Prerequisites
To follow along with this guide, you should be comfortable with:
- React fundamentals (hooks, components)
- TypeScript basics (enums, generics)
- Redux Toolkit or any global state management tool
- Basic understanding of Role-Based Access Control (RBAC) principles
- Familiarity with Higher-Order Components (HOCs) in React
Problem Context
At work, we manage internal tools where different users have varying access levels. For instance:
- Admins can manage users and view reports
- Merchants can only manage their store
- Support agents can handle tickets but not access sensitive data
Rather than scatter if (user.role === 'admin')
all over the app, I wanted a reusable way to:
- Centralize permission checks
- Disable or hide UI elements based on permissions
- Keep code clean and declarative
Step 1: Define Your Permissions
First, we define all possible permissions in a single, centralized enum. This makes it easy to reuse and manage across your app.
// permissions.ts
export enum Permission {
ProductCreate = 'product.create',
ProductDelete = 'product.delete',
ProductUpdate = 'product.update',
ProductView = 'product.view',
OrdersView = 'orders.view',
OrdersManage = 'orders.manage',
UsersCreate = 'users.create',
UsersDelete = 'users.delete',
RolesUpdate = 'roles.update',
ApiKeysRead = 'apikeys.read',
ApiKeysCreate = 'apikeys.create',
}
// Flatten the enum into an array if needed elsewhere in your app
export const ALL_PERMISSIONS: Permission[] = Object.values(Permission);
Step 2: Create a Central Permission Checker
Build a utility function that checks whether the current user has a specific permission. This helps us avoid repeating logic in multiple components.
// utils/hasPermissions.ts
export const hasPermission = (
user: User | null,
requiredPermission: Permission,
allPermissions: Permission[]
): boolean => {
if (!allPermissions.includes(requiredPermission)) {
return false;
}
return user?.permissions?.includes(requiredPermission) ?? false;
};
Step 3: Create a Higher-Order Component for Permissions
The core logic that wraps any component and disables it based on permission.
// hocs/withPermission.tsx
const withPermission = <P extends object>(
action: Permission,
Component: React.ComponentType<P & { disabled?: boolean }>
) => {
return function WithPermissionWrapper(props: P & { disabled?: boolean }) {
const user = useSelector((state: RootState) => state.user.data);
const isAllowed = hasPermission(user, action, ALL_PERMISSIONS);
const disabled = props.disabled || !isAllowed;
return <Component {...props} disabled={disabled} />;
};
};
export default withPermission;
Why disable instead of hide?
Removing components entirely based on permissions can cause layout breaks and unpredictable UI bugs. By disabling them instead, we preserve the layout, avoid unnecessary conditional rendering, and still communicate clearly to the user that they don’t have access.
Usage Example
Let’s say only users with the DELETE_PRODUCT permission can use a delete button:
import withPermission from '@/hocs/withPermission';
import { Permission } from '@/types/permissions';
import DeleteButton from './DeleteButton';
const DeleteButtonWithPermission = withPermission(Permission.ProductDelete, DeleteButton);
// In JSX
<DeleteButtonWithPermission onClick={handleDelete} />
Why This Works
- Reusability: Wrap any component once, reuse everywhere
- Clean UI code: No inline permission checks in JSX
- Central logic: One source of truth for permission rules
You can also extend this idea:
- Use
withPermission
to hide instead of disable - Wrap entire pages or routes
- Combine it with role groups or feature flags
Conclusion
Role-based UI logic is inevitable in real-world apps — especially internal tools or admin dashboards. By abstracting permission logic with enums, utilities, and HOCs, you can make your React codebase far more readable, scalable, and testable.
Let’s chat,
How are you handling permissions in your React apps?
Have you tried wrapping routes or using context instead of Redux?
Drop a comment below or connect with me — I’d love to learn from other implementations!
Top comments (0)