DEV Community

Victory Ndukwu.
Victory Ndukwu.

Posted on

A Practical Guide to Role-Based Permissions in React.

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);

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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} />

Enter fullscreen mode Exit fullscreen mode

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)