Skip to main content

Permission-Based UI Rendering System Design

Comprehensive low-level design for implementing role-based and attribute-based access control in frontend applications with dynamic UI component rendering.

This document outlines a robust low-level architecture for permission-based UI rendering that dynamically controls component visibility based on user roles and attributes, ensuring secure and flexible frontend access control implementation.

1. Problem Statement & Requirements

Clarifying questions

  1. What's the target browser support matrix? (e.g., IE11, mobile Safari, Chrome 90+)
  2. Do we need to support role-based access control (RBAC) or just simple permission flags?
  3. Should the permission system be real-time (user permissions can change during session) or static (permissions resolved at login)?
  4. Do we need offline support for permission checks when the user loses network connectivity?
  5. What's the expected scale of permissions? Do we expect hundreds of unique permission strings or just dozens?
  6. Are there accessibility requirements beyond WCAG AA compliance (e.g., screen reader support for permission-based content hiding)?
  7. Do we have a design system in place that we should integrate with for consistent UI patterns?

Functional requirements

  • Render UI components conditionally based on user permissions
  • Support granular permission checking (e.g., "can-view-users", "can-edit-settings")
  • Allow permission-based hiding of entire sections or individual UI elements
  • Support hierarchical permissions (e.g., "can-edit-users" implies "can-view-users")
  • Provide fallback UI for unauthorized access attempts
  • Support role-based permission inheritance (e.g., admin inherits all user permissions)
  • Enable runtime permission updates without full page reload

Non-functional requirements

  • Performance: Permission checks should be fast (sub-5ms for 100+ checks)
  • Scalability: Support up to 10,000 unique permission strings
  • Accessibility: Hidden content should be properly excluded from screen readers and keyboard navigation
  • Security: Client-side permission checks are not security boundaries (should never block sensitive operations)
  • Maintainability: Permission system should be easy to extend and debug

Constraints and assumptions

  • Permission data is fetched once at login and cached in client state
  • All permission checks happen synchronously on the main thread
  • UI components can be conditionally rendered but not dynamically created based on permissions
  • Permission data is assumed to be valid and properly formatted by backend services
  • The system assumes a centralized permission store with no cross-feature dependencies

Out of scope

  • Permission assignment and management UI (handled by admin dashboard)
  • Real-time permission updates from server (only supports session-based permissions)
  • Complex permission logic like "can-view-users if user is in group X and has role Y"
  • Permission caching across browser sessions (only session-based)
  • Multi-tenant permission support (single tenant only)
  • Permission audit logging or change tracking
  • Integration with external identity providers beyond token-based auth

2. Component Architecture

graph TD
    A[PermissionProvider] --> B[PermissionGuard]
    A --> C[UserContext]
    B --> D[ProtectedRoute]
    B --> E[PermissionCheck]
    E --> F[AuthorizedComponent]
    E --> G[UnauthorizedComponent]
    D --> H[ProtectedComponent]
    H --> I[FeatureComponent]
    I --> J[UIElement]

Component Tree and Breakdown

The permission-based UI rendering system consists of three primary component types:

  1. Smart Components (Container Components):

    • PermissionProvider: Context provider that manages user permissions and role mappings
    • PermissionGuard: Route-level guard that checks permissions before rendering protected routes
    • ProtectedRoute: Higher-order component that wraps route components with permission checks
  2. Dumb Components (Presentational Components):

    • AuthorizedComponent: Renders content when user has required permissions
    • UnauthorizedComponent: Renders fallback UI when user lacks permissions
    • FeatureComponent: Individual feature components that may be conditionally rendered

TypeScript Props Contracts

// PermissionProvider props
interface PermissionProviderProps {
  permissions: string[];
  roles: string[];
  children: React.ReactNode;
}

// PermissionGuard props
interface PermissionGuardProps {
  requiredPermissions: string[];
  fallback?: React.ReactNode;
  renderUnauthorized?: () => React.ReactNode;
  component: React.ComponentType<any>;
}

// ProtectedRoute props
interface ProtectedRouteProps {
  path: string;
  requiredPermissions: string[];
  component: React.ComponentType<any>;
  fallback?: React.ReactNode;
}

// FeatureComponent props
interface FeatureComponentProps {
  permissions: string[];
  requiredPermissions: string[];
  renderAuthorized?: () => React.ReactNode;
  renderUnauthorized?: () => React.ReactNode;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> CheckingPermissions: checkPermissions()
    CheckingPermissions --> Authorized: permissionsValid
    CheckingPermissions --> Unauthorized: permissionsInvalid
    Authorized --> Idle: resetPermissions()
    Unauthorized --> Idle: resetPermissions()

Component Responsibilities

  • PermissionProvider:

    • Manages global permission context
    • Provides permission checking utility to child components
    • Handles permission updates from API or authentication flows
  • PermissionGuard:

    • Wraps route components with permission validation
    • Renders fallback UI when permissions are insufficient
    • Handles conditional rendering based on user permissions
  • ProtectedRoute:

    • Implements route-level permission checks
    • Manages navigation to unauthorized fallbacks
    • Integrates with routing system for protected paths
  • AuthorizedComponent:

    • Renders content when user has required permissions
    • Handles loading states during permission validation
  • UnauthorizedComponent:

    • Renders fallback UI for insufficient permissions
    • Provides user guidance or redirect options

Reusability Decisions

The system prioritizes reusability through:

  • Context-based permission access for any component in the tree
  • Route-level guards that can be composed with different components
  • Modular fallback UIs that can be customized per application needs

Component Lifecycle Considerations

  1. Mounting: PermissionProvider initializes with user permissions
  2. Updating: Permission changes trigger re-renders of guarded components
  3. Unmounting: Cleanup of permission listeners and subscriptions
  4. Error Boundaries: PermissionGuard wraps components with error handling for permission validation failures

The design supports both synchronous and asynchronous permission loading, with proper loading states and error handling throughout the component lifecycle.

3. State Management Design

Local state vs. global state decisions

For permission-based UI rendering, we need to distinguish between:

  • Local state: Component-specific permissions (e.g., user's current role in a specific context)
  • Global state: System-wide permission data (user roles, permissions cache, feature flags)

The core permission data should be global to avoid prop drilling and ensure consistency across the application. Individual component permissions (like "canEdit" for a specific item) can be local or derived from global state.

State management approach

Zustand is selected as the primary state management solution because:

  1. Simplicity: Minimal boilerplate and easy to understand
  2. TypeScript support: First-class TypeScript integration with strong typing
  3. Flexibility: Can handle both local and global state patterns
  4. Performance: Optimized for React with minimal re-renders
  5. No provider wrapping required: Unlike Context API, no need to wrap entire app

State shape and normalization strategy

// Normalized state structure for permission management
interface UserPermissions {
  roles: string[];
  permissions: string[];
  features: Record<string, boolean>;
}

interface PermissionState {
  userPermissions: UserPermissions | null;
  permissionsLoaded: boolean;
  permissionsError: string | null;
  featureFlags: Record<string, boolean>;
  resourcePermissions: Record<string, string[]>;
}

Server state vs. client state separation

  • Server state: User's role and permission data from authentication service
  • Client state:
    • Cached permissions for fast UI rendering
    • Feature flag states (fetched from backend or config)
    • Local permission overrides for testing

Cache invalidation strategy

  1. Token expiration: Invalidate cache when auth token expires
  2. Manual refresh: Allow manual permission refresh via UI or API call
  3. Role change detection: Invalidate cache when user role changes
  4. Time-based refresh: Refresh permissions every 24 hours or on app focus

Optimistic updates design

When a user's permissions change:

  1. Optimistically update UI: Show new permissions immediately
  2. Validate with backend: Fetch fresh permissions to confirm changes
  3. Handle conflicts: If backend differs, rollback and show error

Critical-path hook implementation

// PermissionManager Hook - Core state management for permission-based UI rendering
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface PermissionState {
  userPermissions: UserPermissions | null;
  permissionsLoaded: boolean;
  permissionsError: string | null;
  featureFlags: Record<string, boolean>;
  resourcePermissions: Record<string, string[]>;
  loading: boolean;
}

interface PermissionActions {
  initializePermissions: () => Promise<void>;
  refreshPermissions: () => Promise<void>;
  hasPermission: (permission: string) => boolean;
  canAccessResource: (resourceId: string, requiredPermission: string) => boolean;
  hasFeatureFlag: (feature: string) => boolean;
  setFeatureFlag: (feature: string, enabled: boolean) => void;
  setUserPermissions: (permissions: UserPermissions) => void;
}

const usePermissionStore = create<PermissionState & PermissionActions>(
  persist(
    (set, get) => ({
      userPermissions: null,
      permissionsLoaded: false,
      permissionsError: null,
      featureFlags: {},
      resourcePermissions: {},
      loading: false,

      initializePermissions: async () => {
        set({ loading: true, permissionsError: null });
        
        try {
          // Fetch from auth service or local storage
          const permissions = await fetchUserPermissions();
          
          set({
            userPermissions: permissions,
            permissionsLoaded: true,
            loading: false
          });
        } catch (error) {
          set({
            permissionsError: error instanceof Error ? error.message : 'Failed to load permissions',
            loading: false
          });
        }
      },

      refreshPermissions: async () => {
        set({ loading: true, permissionsError: null });
        
        try {
          const permissions = await fetchUserPermissions();
          set({
            userPermissions: permissions,
            permissionsLoaded: true,
            loading: false
          });
        } catch (error) {
          set({
            permissionsError: error instanceof Error ? error.message : 'Failed to refresh permissions',
            loading: false
          });
        }
      },

      hasPermission: (permission: string): boolean => {
        const { userPermissions } = get();
        return !!userPermissions?.permissions.includes(permission);
      },

      canAccessResource: (resourceId: string, requiredPermission: string): boolean => {
        const { resourcePermissions, userPermissions } = get();
        
        // Check direct resource permissions
        const resourcePerms = resourcePermissions[resourceId] || [];
        if (resourcePerms.includes(requiredPermission)) {
          return true;
        }
        
        // Fallback to user global permissions
        return !!userPermissions?.permissions.includes(requiredPermission);
      },

      hasFeatureFlag: (feature: string): boolean => {
        const { featureFlags } = get();
        return featureFlags[feature] === true;
      },

      setFeatureFlag: (feature: string, enabled: boolean) => {
        set((state) => ({
          featureFlags: {
            ...state.featureFlags,
            [feature]: enabled
          }
        }));
      },

      setUserPermissions: (permissions: UserPermissions) => {
        set({ userPermissions: permissions });
      }
    }),
    {
      name: 'permission-storage',
      partialize: (state) => ({
        userPermissions: state.userPermissions,
        featureFlags: state.featureFlags
      })
    }
  )
);

// Custom hook for permission checking in components
export const usePermission = () => {
  const {
    userPermissions,
    permissionsLoaded,
    permissionsError,
    loading,
    hasPermission,
    canAccessResource,
    hasFeatureFlag,
    setFeatureFlag
  } = usePermissionStore();

  return {
    userPermissions,
    permissionsLoaded,
    permissionsError,
    loading,
    hasPermission,
    canAccessResource,
    hasFeatureFlag,
    setFeatureFlag
  };
};

// Permission context provider for component tree
export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const { initializePermissions } = usePermissionStore();
  
  useEffect(() => {
    initializePermissions();
  }, [initializePermissions]);

  return <>{children}</>;
};

// Permission guard component
export const PermissionGuard: React.FC<{
  permissions: string[];
  resource?: string;
  fallback?: React.ReactNode;
  children: React.ReactNode;
}> = ({ permissions, resource, fallback, children }) => {
  const { hasPermission, canAccessResource } = usePermission();
  
  // Check if user has any of the required permissions
  const hasAccess = permissions.some(perm => 
    resource ? canAccessResource(resource, perm) : hasPermission(perm)
  );
  
  if (!hasAccess && fallback) {
    return <>{fallback}</>;
  }
  
  return <>{hasAccess ? children : null}</>;
};

// Feature flag guard component
export const FeatureFlagGuard: React.FC<{
  feature: string;
  fallback?: React.ReactNode;
  children: React.ReactNode;
}> = ({ feature, fallback, children }) => {
  const { hasFeatureFlag } = usePermission();
  
  if (!hasFeatureFlag(feature) && fallback) {
    return <>{fallback}</>;
  }
  
  return <>{hasFeatureFlag(feature) ? children : null}</>;
};

// Example usage in a component
const UserDashboard: React.FC = () => {
  const { loading, permissionsLoaded } = usePermission();
  
  if (loading) return <div>Loading permissions...</div>;
  if (!permissionsLoaded) return <div>Failed to load permissions</div>;
  
  return (
    <div>
      <PermissionGuard permissions={['view_users']}>
        <UserList />
      </PermissionGuard>
      
      <FeatureFlagGuard feature="new_ui">
        <NewDashboard />
      </FeatureFlagGuard>
    </div>
  );
};

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    B --> B1[Landing Page]
    B --> B2[Login]
    B --> B3[Register]
    C --> C1[Dashboard Layout]
    C1 --> C2[Home]
    C1 --> C3[Profile]
    C1 --> C4[Settings]
    C1 --> C5[Admin]
    C5 --> C51[User Management]
    C5 --> C52[Role Configuration]

The routing structure follows a clear hierarchy:

  • Public routes (Landing, Login, Register) are accessible without authentication
  • Protected routes are wrapped in authentication guards and require valid session tokens
  • Nested route structure provides clean organization for dashboard components

Code Splitting Strategy

Route-based code splitting with dynamic imports:

// routes/index.tsx
import { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';

const DashboardLayout = lazy(() => import('layouts/DashboardLayout'));
const Home = lazy(() => import('pages/Home'));
const Profile = lazy(() => import('pages/Profile'));
const Settings = lazy(() => import('pages/Settings'));
const Admin = lazy(() => import('pages/Admin'));
const UserManagement = lazy(() => import('pages/Admin/UserManagement'));
const RoleConfiguration = lazy(() => import('pages/Admin/RoleConfiguration'));

// Main routing component with suspense fallbacks
export const AppRoutes = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/register" element={<Register />} />
        <Route element={<ProtectedRoute />}>
          <Route path="/dashboard" element={<DashboardLayout />}>
            <Route index element={<Home />} />
            <Route path="profile" element={<Profile />} />
            <Route path="settings" element={<Settings />} />
            <Route path="admin" element={<Admin />}>
              <Route path="users" element={<UserManagement />} />
              <Route path="roles" element={<RoleConfiguration />} />
            </Route>
          </Route>
        </Route>
      </Routes>
    </Suspense>
  );
};

Justification:

  • Route-based splitting ensures that each protected route's bundle is only loaded when needed
  • Admin routes are split into their own chunk since they're less frequently accessed and contain heavy dependencies
  • Suspense fallback provides better UX during code loading
// components/ProtectedRoute.tsx
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/authStore';

interface ProtectedRouteProps {
  children: React.ReactNode;
}

export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
  const [isAuthorized, setIsAuthorized] = useState<boolean | null>(null);
  const navigate = useNavigate();
  const { isAuthenticated, user, checkAuthStatus } = useAuthStore();

  useEffect(() => {
    const validateAuth = async () => {
      if (!isAuthenticated) {
        navigate('/login', { replace: true });
        setIsAuthorized(false);
        return;
      }

      // Check permission requirements for current route
      const hasPermission = await checkUserPermissions();
      setIsAuthorized(hasPermission);
      
      if (!hasPermission) {
        navigate('/unauthorized', { replace: true });
      }
    };

    validateAuth();
  }, [isAuthenticated, navigate]);

  if (isAuthorized === null) {
    return <div>Loading...</div>;
  }

  return isAuthorized ? <>{children}</> : <div>Access Denied</div>;
};

// Permission validation logic
const checkUserPermissions = async (): Promise<boolean> => {
  // Implementation would check user's role against route permissions
  const userRole = await getCurrentUserRole();
  const requiredPermissions = getRoutePermissions(); // From route metadata
  
  return userRole && hasRequiredPermissions(userRole, requiredPermissions);
};

Deep Linking Support and URL Parameter Design

// types/route-types.ts
export interface RouteParams {
  userId?: string;
  roleId?: string;
  page?: string;
  search?: string;
}

export interface RouteQueryParams {
  tab?: 'overview' | 'settings' | 'activity';
  filter?: string;
  sort?: 'asc' | 'desc';
}

// URL structure examples:
// /dashboard/users/123?tab=overview&filter=admin
// /admin/roles/456?sort=desc

Browser History Management and Scroll Restoration

// components/ScrollToTop.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const ScrollToTop = () => {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
};

// In App.tsx
const App = () => {
  return (
    <>
      <ScrollToTop />
      <AppRoutes />
    </>
  );
};

Permission-Based Route Configuration

// types/permission-types.ts
export type Permission = 'read:user' | 'write:user' | 'delete:user' | 'manage:roles';
export type Role = 'admin' | 'editor' | 'viewer';

export interface RoutePermissionConfig {
  path: string;
  requiredPermissions: Permission[];
  allowedRoles?: Role[];
}

// Route configuration with permission metadata
export const routePermissions: RoutePermissionConfig[] = [
  {
    path: '/dashboard',
    requiredPermissions: ['read:user'],
    allowedRoles: ['admin', 'editor', 'viewer']
  },
  {
    path: '/admin/users',
    requiredPermissions: ['manage:user'],
    allowedRoles: ['admin']
  },
  {
    path: '/admin/roles',
    requiredPermissions: ['manage:roles'],
    allowedRoles: ['admin']
  }
];
// hooks/useNavigation.ts
import { useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';

export const usePermissionedNavigation = () => {
  const navigate = useNavigate();
  const location = useLocation();

  const navigateWithPermissionCheck = useCallback(
    (path: string, options?: { replace?: boolean; state?: any }) => {
      // Check if route requires permissions
      const routeConfig = routePermissions.find(r => r.path === path);
      
      if (routeConfig) {
        const hasPermission = checkUserHasPermission(routeConfig.requiredPermissions);
        
        if (!hasPermission) {
          // Redirect to unauthorized page or show error
          navigate('/unauthorized', { replace: true });
          return;
        }
      }
      
      navigate(path, options);
    },
    [navigate]
  );

  return { navigateWithPermissionCheck };
};

This routing design provides:

  • Clean separation of public and protected routes
  • Efficient code splitting with proper fallbacks
  • Comprehensive permission checking at route level
  • Support for deep linking and URL parameter handling
  • Proper scroll restoration and navigation guards
  • Extensible permission configuration system

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using Axios with a custom fetch wrapper for better type safety and error handling. The client will be built as a singleton pattern to ensure consistent configuration across the application.

// api/client.ts
import axios, { AxiosInstance } from 'axios';

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor for auth token injection
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('authToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor for error handling and normalization
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          // Handle token expiration
          localStorage.removeItem('authToken');
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }

  public get<T>(url: string): Promise<T> {
    return this.client.get<T>(url).then((res) => res.data);
  }

  public post<T>(url: string, data?: any): Promise<T> {
    return this.client.post<T>(url, data).then((res) => res.data);
  }

  public put<T>(url: string, data?: any): Promise<T> {
    return this.client.put<T>(url, data).then((res) => res.data);
  }

  public delete<T>(url: string): Promise<T> {
    return this.client.delete<T>(url).then((res) => res.data);
  }
}

export const apiClient = new ApiClient();

Request/Response Interceptors

The API client implements interceptors for authentication token injection and centralized error handling. Request interceptors inject the Bearer token from localStorage, while response interceptors handle 401 Unauthorized responses by redirecting to login.

Error Handling and Retry Strategy

We implement a retry mechanism with exponential backoff for transient network errors:

// api/retry.ts
export const retryWithBackoff = async <T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> => {
  let lastError: Error;
  
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      
      if (i === maxRetries) break;
      
      // Retry only for network errors or 5xx status codes
      if (error?.response?.status >= 500 || error?.code === 'ECONNABORTED') {
        const delay = baseDelay * Math.pow(2, i);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      
      throw error;
    }
  }
  
  throw lastError;
};

Loading, Error, and Empty States Design

We implement a standardized loading state pattern using React Query's built-in loading states and custom error boundaries:

// components/LoadingState.tsx
import { Spinner } from './Spinner';

interface LoadingStateProps {
  isLoading: boolean;
  error?: Error | null;
  children: React.ReactNode;
}

export const LoadingState: React.FC<LoadingStateProps> = ({ 
  isLoading, 
  error, 
  children 
}) => {
  if (isLoading) return <Spinner />;
  
  if (error) {
    return (
      <div className="error-container">
        <p>Something went wrong</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }
  
  return <>{children}</>;
};

Data Fetching Strategy

For permission-based UI, we'll use React Query with a hybrid approach:

  • User permissions: Fetch once on login and cache in global state (CSR)
  • Dynamic UI elements: Fetch per route or component with appropriate caching
  • Static content: Use SSG where possible for better performance
// hooks/useUserPermissions.ts
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../api/client';

interface UserPermission {
  resource: string;
  action: string;
  allowed: boolean;
}

export const useUserPermissions = () => {
  return useQuery<UserPermission[]>({
    queryKey: ['user-permissions'],
    queryFn: () => apiClient.get<UserPermission[]>('/auth/permissions'),
    staleTime: 1000 * 60 * 5, // 5 minutes
    cacheTime: 1000 * 60 * 10, // 10 minutes
  });
};

API Response Transformation Layer

We implement a DTO-to-domain model mapping layer:

// api/mappers.ts
import { UserPermission } from '../types/permissions';

export const mapPermissionsResponse = (
  rawPermissions: any[]
): UserPermission[] => {
  return rawPermissions.map((perm) => ({
    resource: perm.resource,
    action: perm.action,
    allowed: perm.allowed,
    id: perm.id,
  }));
};

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant C as Component
    participant Q as React Query
    participant A as API Client
    participant S as Server

    U->>C: Component mounts
    C->>Q: Query user permissions
    Q->>A: Fetch permissions from API
    A->>S: GET /auth/permissions
    S-->>A: [resource, action, allowed]
    A-->>Q: Response with permissions
    Q-->>C: Update UI based on permissions
    C->>U: Render appropriate UI elements

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for 95% of users
  • Interaction to Next Paint (INP): <200ms for 90% of interactions
  • Cumulative Layout Shift (CLS): <0.1 for 95% of users

Bundle Size Budget per Route

  • Home/Overview: <200KB (including dependencies)
  • Dashboard: <350KB (with charts and tables)
  • Settings/Permissions: <250KB (with form components)
  • Admin Panel: <450KB (with heavy UI libraries)

Critical Rendering Path Optimization

  • Critical CSS: Inline essential styles for above-the-fold content
  • Resource Hints: Preload key fonts and critical assets
  • Code Splitting: Route-based splitting with dynamic imports
  • Lazy Loading: Non-critical components load on demand

Image Optimization Strategy

  • Formats: WebP for modern browsers, JPEG fallback for legacy
  • Lazy Loading: loading="lazy" attribute on all images
  • Responsive Images: srcSet with multiple sizes based on viewport
  • Image Compression: 20% reduction in file size without perceptible quality loss

Font Loading Strategy

  • Font Display: swap for all fonts to prevent FOIT
  • Preload Critical Fonts: <link rel="preload"> for primary font families
  • Font Subsets: Unicode ranges for language-specific character sets
  • Fallback Fonts: System fonts as immediate fallbacks

Memoization Strategy

// Memoize expensive computations in components
const MemoizedPermissionCheck = React.memo(({ user, resource }: Props) => {
  const hasPermission = useMemo(() => checkUserPermissions(user, resource), [user, resource]);
  
  return <div>{hasPermission ? 'Authorized' : 'Unauthorized'}</div>;
});

// Use useCallback for event handlers
const handlePermissionChange = useCallback((permission) => {
  // Handle permission update logic
}, []);

Virtualization for Large Lists

// For large permission lists, use react-window
import { FixedSizeList as List } from 'react-window';

const PermissionList = ({ permissions }: { permissions: Permission[] }) => {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      {permissions[index].name}
    </div>
  );

  return (
    <List
      height={400}
      itemCount={permissions.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </List>
  );
};

Prefetching and Preloading Strategy

  • Permission Data: Fetch user permissions on app initialization
  • Route Preload: Preload next route's components when hovering over navigation links
  • Critical Data: Fetch permission hierarchy before rendering protected components
  • Background Sync: Preload permission updates during idle periods

Performance Monitoring Implementation

// Track performance metrics with PerformanceObserver
const observePerformance = () => {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.entryType === 'largest-contentful-paint') {
        // Send LCP metric to monitoring service
        sendMetric('lcp', entry.startTime);
      }
    }
  });

  observer.observe({ entryTypes: ['largest-contentful-paint'] });
};

// Measure component render times
const withPerformanceTracking = (Component) => {
  return (props) => {
    const [renderTime, setRenderTime] = useState(0);
    
    useEffect(() => {
      const start = performance.now();
      // Component renders
      const end = performance.now();
      setRenderTime(end - start);
    }, []);

    return <Component {...props} renderTime={renderTime} />;
  };
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The placement follows a hierarchical approach:

  1. Application-level error boundary - Catches unhandled errors in the root component tree
  2. Feature-level error boundaries - Wrap major feature components (e.g., dashboard, settings)
  3. Component-level error boundaries - For critical sub-components (e.g., charts, tables)
  4. Async operation error boundaries - Handle specific API call failures
graph TD
    A[App Root] --> B[ErrorBoundary]
    B --> C[Dashboard Feature]
    B --> D[Settings Feature]
    C --> E[ErrorBoundary]
    E --> F[Chart Component]
    E --> G[Table Component]

Global Error Handler Design

A centralized error handler manages all uncaught errors and provides consistent feedback:

// ErrorHandlerContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';

interface ErrorContextType {
  error: Error | null;
  setError: (error: Error) => void;
  clearError: () => void;
}

const ErrorContext = createContext<ErrorContextType | undefined>(undefined);

export const useErrorHandling = () => {
  const context = useContext(ErrorContext);
  if (!context) {
    throw new Error('useErrorHandling must be used within ErrorProvider');
  }
  return context;
};

export const ErrorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      setError(event.error);
    };

    const handlePromiseRejection = (event: PromiseRejectionEvent) => {
      setError(event.reason);
    };

    window.addEventListener('error', handleError);
    window.addEventListener('unhandledrejection', handlePromiseRejection);

    return () => {
      window.removeEventListener('error', handleError);
      window.removeEventListener('unhandledrejection', handlePromiseRejection);
    };
  }, []);

  const clearError = () => {
    setError(null);
  };

  return (
    <ErrorContext.Provider value={{ error, setError, clearError }}>
      {children}
      {error && <GlobalErrorDisplay error={error} onClear={clearError} />}
    </ErrorContext.Provider>
  );
};

Offline / Network Failure Handling

Offline support uses service workers and network status detection:

// OfflineHandler.ts
import { useEffect, useState } from 'react';

export const useOfflineStatus = () => {
  const [isOffline, setIsOffline] = useState(false);

  useEffect(() => {
    const handleOnline = () => setIsOffline(false);
    const handleOffline = () => setIsOffline(true);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    // Check initial status
    setIsOffline(!navigator.onLine);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline, handleOffline');
    };
  }, []);

  return isOffline;
};

// OfflineFallbackUI.tsx
export const OfflineFallback: React.FC = () => (
  <div className="offline-fallback">
    <h2>Offline Mode</h2>
    <p>You're currently offline. Some features may be limited.</p>
    <button onClick={() => window.location.reload()}>
      Refresh to reconnect
    </button>
  </div>
);

Graceful Degradation Patterns

For permission-based UI rendering, fallbacks ensure core functionality remains available:

// PermissionAwareComponent.tsx
interface PermissionCheckProps {
  requiredPermissions: string[];
  fallback?: React.ReactNode;
  children: React.ReactNode;
}

export const PermissionAwareComponent: React.FC<PermissionCheckProps> = ({
  requiredPermissions,
  fallback,
  children
}) => {
  const { hasPermission } = usePermissionChecker();
  const [hasAccess, setHasAccess] = useState<boolean>(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const checkPermissions = async () => {
      try {
        const access = await hasPermission(requiredPermissions);
        setHasAccess(access);
        setLoading(false);
      } catch (error) {
        console.error('Permission check failed:', error);
        setHasAccess(false);
        setLoading(false);
      }
    };

    checkPermissions();
  }, [requiredPermissions, hasPermission]);

  if (loading) {
    return <div className="loading">Checking permissions...</div>;
  }

  if (hasAccess) {
    return <>{children}</>;
  }

  if (fallback) {
    return <>{fallback}</>;
  }

  // Default fallback for missing permissions
  return (
    <div className="permission-denied">
      <p>You don't have permission to view this content.</p>
      <p>Contact your administrator for access.</p>
    </div>
  );
};

User Feedback on Errors

Comprehensive error feedback includes:

  1. Toast notifications for non-critical errors
  2. Inline error messages for form validation
  3. Fallback UIs for missing content
  4. Error logs for debugging
// ToastErrorDisplay.tsx
import { useEffect, useRef } from 'react';

interface ToastErrorProps {
  error: Error;
  onClear: () => void;
}

export const ToastErrorDisplay: React.FC<ToastErrorProps> = ({ error, onClear }) => {
  const toastRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const timer = setTimeout(() => {
      onClear();
    }, 5000);

    return () => clearTimeout(timer);
  }, [onClear]);

  return (
    <div 
      ref={toastRef}
      className="toast-error"
      role="alert"
      aria-live="assertive"
    >
      <div className="toast-content">
        <h3>Something went wrong</h3>
        <p>{error.message}</p>
        <button onClick={onClear} aria-label="Dismiss error">
          ×
        </button>
      </div>
    </div>
  );
};

// GlobalErrorDisplay.tsx
export const GlobalErrorDisplay: React.FC<{ 
  error: Error; 
  onClear: () => void 
}> = ({ error, onClear }) => (
  <div className="global-error">
    <div className="error-container">
      <h2>Unexpected Error</h2>
      <p>We're sorry, but something went wrong.</p>
      <pre className="error-stack">{error.stack}</pre>
      <button onClick={onClear} className="retry-button">
        Try Again
      </button>
    </div>
  </div>
);

Error Recovery Strategies

  1. Automatic retry for transient network errors
  2. Manual refresh for permission-related issues
  3. State persistence to restore UI after recovery
// ErrorRecoveryHook.ts
import { useState, useCallback } from 'react';

interface RecoveryState {
  canRecover: boolean;
  recoveryAction: (() => void) | null;
}

export const useErrorRecovery = (): RecoveryState => {
  const [recoveryAction, setRecoveryAction] = useState<(() => void) | null>(null);
  const [canRecover, setCanRecover] = useState(false);

  const setupRecovery = useCallback((action: () => void) => {
    setRecoveryAction(() => action);
    setCanRecover(true);
  }, []);

  const triggerRecovery = useCallback(() => {
    if (recoveryAction) {
      recoveryAction();
      setCanRecover(false);
      setRecoveryAction(null);
    }
  }, [recoveryAction]);

  return {
    canRecover,
    recoveryAction: triggerRecovery
  };
};

8. Testing Strategy

Unit Testing Approach

Unit tests will cover 85%+ of component logic and utility functions. Each component will have:

  • Component rendering: Verify correct DOM structure with proper props
  • State transitions: Test UI behavior changes based on state updates
  • Event handlers: Validate proper callback invocation and argument passing
  • Prop validation: Ensure components handle edge cases gracefully
// Example: PermissionGate component unit test
describe('PermissionGate', () => {
  const mockPermissions = ['read:users', 'write:users'];
  
  it('renders children when user has required permission', () => {
    const { container } = render(
      <PermissionGate permissions={['read:users']}>
        <div data-testid="protected-content">Protected Content</div>
      </PermissionGate>
    );
    
    expect(container.firstChild).toBeInTheDocument();
  });
  
  it('hides children when user lacks required permission', () => {
    const { queryByTestId } = render(
      <PermissionGate permissions={['delete:users']}>
        <div data-testid="protected-content">Protected Content</div>
      </PermissionGate>
    );
    
    expect(queryByTestId('protected-content')).not.toBeInTheDocument();
  });
});

Integration Testing Strategy

Focus on:

  • Permission context providers with mocked auth state
  • Nested permission hierarchies and inheritance logic
  • Component composition patterns with multiple permission checks
  • State management integration with global store

E2E Testing Scope

End-to-end tests will cover:

  • User role transitions: Admin → Editor → Viewer workflows
  • Permission inheritance: Group-based permissions and cascading rules
  • Real-time permission updates: Simulate backend permission refreshes
  • Protected route access: Verify unauthorized navigation blocking

Using Playwright for E2E testing due to:

  • Native support for modern web features (Shadow DOM, Web Components)
  • Excellent TypeScript integration and type safety
  • Built-in network interception for API mocking
  • Cross-browser testing capabilities

Visual Regression Testing

Implement Storybook + Chromatic pipeline:

  • Test all permission states (granted/denied) for each component
  • Capture UI variations across different role combinations
  • Monitor visual changes in protected content rendering

Accessibility Testing Automation

Integrate axe-core into CI pipeline:

  • Run accessibility checks on all permission-protected components
  • Validate proper ARIA attributes in denied state UIs
  • Ensure keyboard navigation works regardless of permission status

Test Data Strategy

Use fixtures for:

  • User role profiles: Admin, Editor, Viewer, Guest with varying permissions
  • Permission hierarchies: Nested group permissions and inheritance rules
  • Edge case scenarios: Empty permissions, malformed permission strings, null states
// Test data fixtures
const userFixtures = {
  admin: {
    id: 'admin-123',
    roles: ['admin'],
    permissions: ['read:*', 'write:*', 'delete:*']
  },
  editor: {
    id: 'editor-456',
    roles: ['editor'],
    permissions: ['read:users', 'write:users']
  }
};

Coverage Targets

  • Component coverage: 90%+ line and branch coverage for permission components
  • Utility coverage: 100% coverage for permission validation and matching logic
  • Integration coverage: 80%+ of component interaction flows with auth context
  • E2E coverage: 100% of user journey flows through protected UI paths

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The permission-based UI rendering must meet WCAG 2.1 AA standards:

  • 1.1.1 Non-text Content: All permission-restricted UI elements must have appropriate ARIA labels or fallback text when hidden
  • 1.3.1 Info and Relationships: Proper semantic HTML structure maintained even when components are conditionally rendered
  • 1.4.1 Use of Color: Sufficient color contrast (4.5:1) for permission indicators and visual cues
  • 1.4.3 Contrast (Minimum): All interactive elements must maintain minimum contrast ratios
  • 1.4.12 Text Spacing: Support for custom text spacing when UI elements are rendered conditionally
  • 1.4.13 Content on Hover or Focus: All permission-based dropdowns and modals must be operable via keyboard
  • 2.1.1 Keyboard: Full keyboard navigation support for all permission-restricted UI elements
  • 2.4.1 Bypass Blocks: Skip links to navigate past permission-based navigation sections
  • 2.4.3 Focus Order: Logical focus order maintained when UI elements appear/disappear
  • 2.5.1 Pointer Gestures: Touch-friendly controls for permission-based actions (if applicable)
  • 3.1.1 Language of Page: Proper language attribute handling for translated permission messages
  • 4.1.2 Name, Role, Value: ARIA attributes properly maintained on dynamically rendered elements

Keyboard navigation support

All permission-based UI components must be fully operable via keyboard:

  • Tab Navigation: Components that appear conditionally must maintain proper tab order
  • Enter/Space Activation: Buttons and controls within permission-restricted sections must be operable with keyboard
  • Escape Key Handling: Modals and dropdowns should close when Escape is pressed
  • Focus Management: When permission changes, focus should be appropriately managed to maintain context

Focus management strategy

  • Focus Trapping: When permission-based modals open, focus should be trapped within the modal
  • Focus Restoration: When permission-restricted UI is hidden, focus should return to previous context
  • Auto-Focus: When permission allows new UI elements to appear, appropriate element should receive focus
  • Focus Indicators: Clear visual focus indicators must remain visible even when UI elements change

ARIA roles and attributes plan

  • Role Attributes: role="button" for permission-triggered actions, role="alert" for permission denial messages
  • State Attributes: aria-expanded for dropdowns, aria-hidden for hidden permission elements, aria-disabled for disabled actions
  • Labeling: aria-label and aria-labelledby for all permission-restricted UI elements
  • Live Regions: aria-live="polite" for permission status updates that should be announced to screen readers

Screen reader compatibility

  • Dynamic Content Announcements: Use aria-live regions to announce permission changes
  • Semantic HTML: Maintain proper heading hierarchy, list structures, and form controls even when conditionally rendered
  • Alternative Text: All images within permission-restricted UI must have descriptive alt text
  • Reading Order: Screen readers must read permission messages in logical order when UI elements appear
graph TD
    A[Permission Check] --> B{User Has Permission?}
    B -->|Yes| C[Render UI Component]
    B -->|No| D[Hide Component]
    C --> E[ARIA Roles Applied]
    D --> F[ARIA Hidden Set]
    E --> G[Screen Reader Announced]
    F --> H[Focus Management]

Additional A11y Considerations

  • Permission Denial Messages: When users lack permissions, provide clear, actionable messages that are announced to screen readers
  • Visual Indicators: Non-visual cues must be provided for permission states (e.g., color contrast, focus states)
  • Dynamic Updates: Screen reader announcements when permission state changes dynamically
  • Testing Framework: Integration with axe-core for automated accessibility testing in CI/CD pipeline

Implementation Strategy

All permission-based UI components must implement proper ARIA attributes and keyboard navigation patterns. The design should ensure that when elements are conditionally rendered, screen readers receive appropriate announcements and keyboard users can navigate seamlessly through the permission-restricted UI elements.

10. Security Considerations

XSS Prevention Strategy

Content Sanitization: All user-generated content rendered in the UI must be sanitized using a robust library like DOMPurify. This prevents malicious script execution when content is dynamically inserted into the DOM.

// Example usage in a component
import DOMPurify from 'dompurify';

const sanitizeAndRender = (htmlContent: string) => {
  const cleanHtml = DOMPurify.sanitize(htmlContent);
  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
};

Input Validation: All form inputs and API payloads must undergo strict validation on both client and server side. Client-side validation provides immediate feedback, while server-side validation prevents bypass attempts.

Attribute Escaping: When rendering dynamic attributes (e.g., href, src), ensure proper escaping to prevent attribute injection attacks.

CSRF Protection

Token-Based Protection: Implement CSRF token generation and validation for all state-modifying requests (POST, PUT, DELETE). Tokens should be:

  • Generated per session
  • Included in all protected requests via headers or hidden form fields
  • Validated server-side before processing
// Example CSRF token implementation
const useCsrfToken = () => {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);
  
  useEffect(() => {
    // Fetch CSRF token on component mount
    fetch('/api/csrf-token')
      .then(res => res.json())
      .then(data => setCsrfToken(data.token));
  }, []);

  return csrfToken;
};

Content Security Policy (CSP) Headers

Header Configuration: Implement strict CSP headers to prevent unauthorized script execution and resource loading:

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob:;
  font-src 'self' data:;

Sensitive Data Handling in Frontend State

Data Masking: Never store sensitive user data (passwords, PII) in frontend state. Use secure storage mechanisms where appropriate.

State Cleanup: Implement proper cleanup of sensitive data from React state when components unmount or user logs out.

// Secure state management pattern
const useSecureState = <T>(initialValue: T) => {
  const [state, setState] = useState<T>(initialValue);
  
  // Clear sensitive data on logout
  const clearSensitiveData = () => {
    setState((prev) => {
      // Remove sensitive fields from state
      const cleanState = { ...prev };
      delete cleanState.sensitiveField;
      return cleanState;
    });
  };

  return [state, setState, clearSensitiveData] as const;
};

Input Sanitization Approach

Input Validation:

  • Validate all inputs against expected formats (regex patterns, length limits)
  • Implement rate limiting for API endpoints to prevent abuse
  • Sanitize all dynamic content before rendering

API Payload Protection:

// Input sanitization helper
const sanitizeInput = (input: string): string => {
  // Remove potentially dangerous characters
  return input.replace(/[<>'"&]/g, (char) => {
    const escapeMap: Record<string, string> = {
      '<': '&lt;',
      '>': '&gt;',
      "'": '&#x27;',
      '"': '&quot;',
      '&': '&amp;'
    };
    return escapeMap[char];
  });
};

Frontend Security Best Practices:

  1. Never expose API keys or secrets in frontend code
  2. Implement proper session management with secure cookies
  3. Use HTTPS exclusively for all communications
  4. Validate all user permissions server-side before processing any action
  5. Implement proper error handling that doesn't leak sensitive system information

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Translation keys will follow a hierarchical, descriptive naming convention using dot notation:

  • feature.section.label - for UI labels and text elements
  • feature.section.action - for button/text actions
  • feature.section.error - for error messages and validation text
  • feature.section.tooltip - for tooltip content

Example key structure:

{
  "userProfile.editButton": "Edit Profile",
  "userProfile.saveSuccess": "Profile saved successfully",
  "userProfile.validation.emailRequired": "Email is required",
  "userProfile.modal.title": "Update User Information"
}

RTL Support Considerations

RTL support will be implemented through CSS classes and CSS variables:

  • dir="rtl" attribute on root element for proper text direction
  • CSS :dir(rtl) selectors for direction-specific styling
  • Layout adjustments using margin-inline-start/margin-inline-end instead of margin-left/margin-right
  • Text alignment using text-align: start and text-align: end
  • Flexbox and Grid direction handling with flex-direction: row-reverse for RTL

Date, Number, Currency Formatting Strategy

All date/time formatting will use the Intl.DateTimeFormat API with locale-specific options:

const dateFormatter = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

Currency formatting will be handled with proper locale-specific symbols and grouping separators.

Locale Detection and Switching Strategy

Automatic locale detection will be implemented using:

  1. Browser's navigator.language as primary source
  2. User preference stored in localStorage as fallback
  3. Fallback to 'en-US' for unsupported locales

Locale switching will be handled through:

const switchLocale = (newLocale: string) => {
  localStorage.setItem('preferredLocale', newLocale);
  // Trigger re-render with new locale context
  i18n.changeLanguage(newLocale);
};

The system will support:

  • Dynamic language switching without page reload
  • URL parameter-based locale persistence (e.g., /en/users, /fr/users)
  • Language preference persistence across sessions
  • Fallback mechanisms for missing translations

i18n Implementation Approach

The implementation will leverage react-i18next with:

  • Language detection middleware
  • Translation file structure organized by locale (en-US, fr-FR, es-ES)
  • Fallback language system (default to en-US for missing keys)
  • Pluralization support using ICU message syntax
  • Context-aware translation components

Translation File Organization

Translation files will be organized as:

/src/locales/
  ├── en-US/
  │   ├── common.json
  │   ├── user.json
  │   └── permissions.json
  ├── fr-FR/
  │   ├── common.json
  │   ├── user.json
  │   └── permissions.json
  └── es-ES/
      ├── common.json
      ├── user.json
      └── permissions.json

Translation Key Management

A translation key management system will be implemented:

  • Centralized key registry for tracking usage
  • Automated extraction of translation keys from codebase
  • Translation key validation to prevent missing or unused keys
  • Integration with translation management tools (e.g., Crowdin, Transifex)

Performance Considerations

To minimize i18n performance impact:

  • Translation files will be loaded on-demand per locale
  • Bundle splitting for each language to reduce initial load
  • Caching of translation resources in memory
  • Lazy loading of translation files for less common locales

Testing Strategy

i18n testing will include:

  • Translation completeness checks (missing keys in each locale)
  • RTL layout validation on supported languages
  • Date/time formatting consistency across locales
  • Pluralization rule validation for each language
  • Fallback behavior testing when translations are missing

Accessibility Considerations

i18n will maintain accessibility standards:

  • Screen reader support for translated content
  • Proper ARIA labeling in translated UI elements
  • Language attribute (lang) on translated text elements
  • Focus management across translated content
  • Keyboard navigation in RTL locales

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The permission-based UI rendering system should capture key RUM metrics to understand user experience and feature adoption:

  • Permission Rendering Time: Measure time from permission check completion to UI rendering (target < 100ms for 95% of users)
  • Permission Fetch Latency: Track API call duration for permission data (target < 500ms for 95% of requests)
  • UI Flicker Rate: Count instances where UI content changes abruptly due to permission updates (target < 1% of sessions)
  • Permission Cache Hit Rate: Monitor cache effectiveness for permission data (target > 85% cache hits)
  • Failed Permission Checks: Track count of permission evaluation failures (target < 0.1% error rate)

Error Tracking Integration

Integrate with Sentry or Datadog for comprehensive error monitoring:

// Error tracking integration example
import * as Sentry from '@sentry/react';

const PermissionErrorBoundary = ({ children }: { children: React.ReactNode }) => {
  return (
    <Sentry.ErrorBoundary
      fallback={<ErrorFallback />}
      beforeSend={(event) => {
        // Filter out known permission-related errors
        if (event.exception?.values?.some(e => e.type === 'PermissionError')) {
          return null;
        }
        return event;
      }}
    >
      {children}
    </Sentry.ErrorBoundary>
  );
};

Custom Events and User Journey Tracking

Implement custom event tracking for permission-based interactions:

// Permission event tracking
const trackPermissionEvent = (event: string, payload?: Record<string, any>) => {
  if (window.gtag) {
    window.gtag('event', event, {
      ...payload,
      timestamp: Date.now(),
      userId: getCurrentUserId(),
      permissions: getActivePermissions()
    });
  }
};

// Example usage in permission check
const handlePermissionCheck = async (permission: string) => {
  try {
    const hasPermission = await checkUserPermission(permission);
    trackPermissionEvent('permission_check', {
      permission,
      granted: hasPermission,
      timestamp: Date.now()
    });
    return hasPermission;
  } catch (error) {
    trackPermissionEvent('permission_check_error', {
      permission,
      error: error.message
    });
    throw error;
  }
};

Performance Monitoring and Regression Detection

Implement performance monitoring with automated regression detection:

// Performance monitoring hook
const usePermissionPerformance = () => {
  const [metrics, setMetrics] = useState<PermissionMetrics>({
    renderTime: 0,
    cacheHits: 0,
    apiCalls: 0
  });

  const trackRenderPerformance = (renderTime: number) => {
    setMetrics(prev => ({
      ...prev,
      renderTime,
      cacheHits: prev.cacheHits + 1
    }));

    // Alert on performance regressions
    if (renderTime > 200) {
      console.warn(`Permission rendering took ${renderTime}ms - potential regression`);
      // Send to monitoring service
    }
  };

  return { metrics, trackRenderPerformance };
};

interface PermissionMetrics {
  renderTime: number;
  cacheHits: number;
  apiCalls: number;
}

Dashboard and Alerting Strategy

Create dashboards for monitoring permission system health:

  • Permission API Response Time: 95th percentile response times
  • Cache Hit Rate: Percentage of cached vs. fresh permission requests
  • Permission Check Success Rate: % of successful permission evaluations
  • UI Rendering Errors: Count and type of rendering failures
  • User Session Permission Changes: Frequency of permission updates per session

Integration with Existing Observability Stack

The monitoring system should integrate with existing infrastructure:

  • Log Level Control: Different log levels for permission-related operations (DEBUG, INFO, WARN)
  • Metric Aggregation: Aggregate metrics by user role, permission scope, and feature area
  • Alerting Rules:
    • Alert when cache hit rate drops below 80%
    • Alert when permission check latency exceeds 1s
    • Alert when UI rendering errors exceed 0.5% of sessions

Data Retention and Privacy Considerations

  • Retain permission-related metrics for 90 days for trend analysis
  • Anonymize user identifiers in monitoring data
  • Exclude sensitive permission names from logs and dashboards
  • Comply with privacy regulations (GDPR, CCPA) in data collection

Observability Testing Strategy

Implement observability testing to ensure monitoring is working correctly:

// Test for permission monitoring
describe('Permission Monitoring', () => {
  it('should track permission check events', async () => {
    const mockTrack = jest.fn();
    const { result } = renderHook(() => usePermissionTracking(), {
      wrapper: ({ children }) => (
        <MockProvider>
          {children}
        </MockProvider>
      )
    });

    await act(async () => {
      await result.current.checkPermission('read:users');
    });

    expect(mockTrack).toHaveBeenCalledWith(
      'permission_check',
      expect.objectContaining({
        permission: 'read:users',
        granted: true
      })
    );
  });
});

13. File & Folder Structure

Proposed Directory Structure

/src
├── features/
│   ├── permissions/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── services/
│   │   ├── types/
│   │   └── utils/
├── shared/
│   ├── components/
│   ├── hooks/
│   ├── types/
│   └── utils/
├── app/
│   ├── routes/
│   ├── layouts/
│   └── providers/
├── assets/
└── styles/

Module Boundaries and Import Rules

  • Feature isolation: Permission-related logic is contained within features/permissions to avoid cross-feature imports
  • Shared components: Reusable UI elements like PermissionGate, RoleBadge live in shared/components
  • Layer-based organization: Components, hooks, and services are grouped by concern within each feature
  • Barrel exports: features/permissions/index.ts exports all public APIs for clean imports

Shared vs Feature-Specific Code Separation

Shared code (reusable across features):

  • Generic permission utilities (canUserPerformAction)
  • Base UI components (PermissionGate, RoleBadge)
  • Common types and interfaces

Feature-specific code:

  • Permission policy definitions
  • Feature-specific hook implementations
  • Route-level permission checking logic

This structure enables clean separation of concerns while maintaining flexibility for future feature additions. The feature-based organization allows teams to own permission logic independently, while shared components ensure consistency across the application's UI.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React (v18+)

  • Justification: Core framework for building UI components with hooks, context, and concurrent rendering capabilities
  • Bundle size impact: ~120KB gzipped
  • Alternatives considered: Preact - smaller bundle but less ecosystem support; Svelte - compile-time benefits but different mental model

2. React Router v6+

  • Justification: Declarative routing with nested routes, dynamic route matching, and built-in support for React 18 features
  • Bundle size impact: ~20KB gzipped
  • Alternatives considered: Reach Router - smaller but less actively maintained; Next.js router - not needed for SPA context

3. React Query v5+

  • Justification: Production-ready data fetching with automatic caching, background updates, and optimistic UI support
  • Bundle size impact: ~25KB gzipped
  • Alternatives considered: SWR - simpler but less feature-rich; Redux Toolkit - more boilerplate for data fetching

4. Zustand v4+

  • Justification: Lightweight state management with minimal boilerplate, ideal for permission state and UI flags
  • Bundle size impact: ~3KB gzipped
  • Alternatives considered: Redux Toolkit - more complex for simple use cases; Jotai - more granular but higher complexity

5. Tailwind CSS v3+

  • Justification: Utility-first CSS framework with excellent developer experience and customization capabilities
  • Bundle size impact: ~10KB gzipped (with purge)
  • Alternatives considered: Styled Components - more complex setup; CSS Modules - less flexible for rapid prototyping

6. TypeScript v5+

  • Justification: Strong typing for better developer experience, refactoring safety, and compile-time error detection
  • Bundle size impact: None (build-time only)
  • Alternatives considered: Flow - less adoption in modern ecosystem; plain JavaScript - less safety

7. Axios v1+

  • Justification: Robust HTTP client with request/response interceptors, automatic JSON parsing, and error handling
  • Bundle size impact: ~15KB gzipped
  • Alternatives considered: Fetch API - built-in but less feature-rich; ky - modern but smaller bundle with less ecosystem support

Alternatives Considered and Rejected

1. Redux Toolkit

  • Rejected: Too much boilerplate for permission state management; overkill for simple UI permission logic
  • Rationale: Permission systems don't require complex middleware or extensive action creators

2. Emotion

  • Rejected: More complex setup than Tailwind; less developer-friendly for rapid UI iteration
  • Rationale: Tailwind's utility-first approach provides better DX and faster prototyping

3. Formik

  • Rejected: Not needed for permission rendering; form-specific library when we need general UI control
  • Rationale: Permission-based UI is about conditional rendering, not form handling

4. React Context API (native)

  • Rejected: Too low-level for permission management; requires extensive boilerplate and manual memoization
  • Rationale: Need structured state management with built-in caching and error boundaries

Licensing and Bundle Size Impact Assessment

Bundle Size Budget Allocation:

  • Core Framework (React + React Router): ~140KB
  • State Management (Zustand + React Query): ~30KB
  • UI Layer (Tailwind CSS): ~10KB
  • HTTP Client (Axios): ~15KB
  • Type Safety (TypeScript): 0KB (build-time)
  • Development Tools: ~5KB (React DevTools, etc.)

Total estimated runtime bundle size: ~200KB gzipped (under 300KB target for optimal LCP)

License Compliance:

  • All libraries are MIT or BSD licensed (compatible with commercial use)
  • No proprietary dependencies that would require enterprise licensing
  • Open source ecosystem with active maintenance and community support

Security Considerations:

  • All libraries are actively maintained with security patch releases
  • Regular security audits and vulnerability scanning in CI pipeline
  • Dependency tree analysis to detect and remediate known vulnerabilities

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Permission granularity vs. performance trade-off: Should we implement fine-grained permission checks at component level or rely on coarse-grained role-based access? This decision directly impacts rendering performance and complexity of permission management.
  • User vs. role-based permissions: Should the system prioritize user-specific permissions or role-based inheritance? This affects how we model and cache permission data in the frontend.
  • Permission caching strategy: How aggressively should we cache permissions? Should they be cached per session, per user, or invalidated on role changes?

Known trade-offs made in this design

  • Component-level permission checks vs. centralized control: Prioritizing component-level checks for granular control but introducing potential performance overhead from checking permissions in multiple places. This trades off developer convenience for more precise control.
  • Static vs. dynamic permission loading: Using static permission mapping in component props rather than runtime API fetches for performance reasons, but this creates a tight coupling between frontend and backend permission definitions.
  • Prop-based vs. context-based permission handling: Using props for permission checks rather than a global context to avoid complexity, but this requires more boilerplate in parent components and makes permission inheritance harder to manage.

Risks and mitigations

  • Permission drift risk: Risk that frontend permission definitions become out of sync with backend. Mitigation: Implement strict CI checks that validate permission keys against backend definitions and enforce permission schema validation.
  • Performance degradation: Risk of excessive permission checks causing rendering bottlenecks. Mitigation: Implement permission memoization and lazy evaluation patterns, with performance monitoring to detect problematic check patterns.
  • Security boundary violation: Risk that UI-level permission checks are bypassed by direct API calls. Mitigation: Always enforce permissions at the backend level and treat frontend checks as UX enhancements only.
  • Complexity explosion: Risk that permission logic becomes unwieldy in deeply nested component trees. Mitigation: Implement permission wrapper components and clear permission hierarchy rules.

Technical debt acknowledged

  • Permission schema rigidity: The current design assumes a fixed permission schema that may not easily accommodate evolving permission requirements without refactoring.
  • Testing complexity: Permission-based UI rendering introduces combinatorial test explosion - each permission combination needs to be tested, which increases testing overhead significantly.
  • Maintenance burden: Permission definitions are scattered across multiple components and files, making maintenance harder as the system scales.
  • Cascading permission updates: When a user's role changes, all components that check permissions need to re-render, which could cause unnecessary performance overhead without proper memoization strategies.

Was this article helpful?

Topics in this article

More from Frontend LLD

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.