System Design & Architecture

Designing component libraries, code-splitting, testing, auth, and data-fetching layers at scale.

How would you design a reusable, accessible component library? What principles would you follow?

Answer:

Designing a reusable, accessible component library requires careful consideration of multiple aspects:

5.1.1. Core Design Principles

  1. Single Responsibility Principle

    • Each component should have one clear purpose
    • Avoid components that try to do too many things
    • Example: Separate Button from ButtonGroup
  2. Composition over Configuration

    • Prefer composition patterns over complex prop interfaces
    • Use render props, children, or compound components
    // Good: Composable
    <Card>
      <Card.Header>Title</Card.Header>
      <Card.Body>Content</Card.Body>
      <Card.Footer>Actions</Card.Footer>
    </Card>
    
    // Avoid: Over-configured
    <Card 
      hasHeader={true} 
      headerText="Title" 
      hasFooter={true} 
      footerContent="Actions"
    />
    
  3. Consistent API Design

    • Standardize prop naming conventions
    • Use consistent patterns across components
    • Example: size, variant, disabled props across all interactive components

5.1.2. Accessibility (a11y) Principles

  1. Semantic HTML

    // Good: Semantic button
    <button 
      type="button" 
      aria-label="Close dialog"
      onClick={onClose}
    >
      <CloseIcon aria-hidden="true" />
    </button>
    
  2. Keyboard Navigation

    • Ensure all interactive elements are keyboard accessible
    • Implement proper focus management
    • Use appropriate ARIA attributes
  3. Screen Reader Support

    const Modal = ({ isOpen, onClose, children }) => {
      const modalRef = useRef<HTMLDivElement>(null);
      
      useEffect(() => {
        if (isOpen && modalRef.current) {
          modalRef.current.focus();
        }
      }, [isOpen]);
      
      return (
        <div 
          role="dialog" 
          aria-modal="true"
          aria-labelledby="modal-title"
          ref={modalRef}
          tabIndex={-1}
        >
          {children}
        </div>
      );
    };
    

5.1.3. TypeScript Integration

  1. Strong Typing

    interface ButtonProps {
      variant: 'primary' | 'secondary' | 'danger';
      size: 'small' | 'medium' | 'large';
      disabled?: boolean;
      loading?: boolean;
      children: React.ReactNode;
      onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
    }
    
    const Button: React.FC<ButtonProps> = ({
      variant,
      size,
      disabled = false,
      loading = false,
      children,
      onClick,
      ...props
    }) => {
      // Implementation
    };
    
  2. Generic Components

    interface ListProps<T> {
      items: T[];
      renderItem: (item: T, index: number) => React.ReactNode;
      keyExtractor: (item: T) => string | number;
    }
    
    function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
      return (
        <ul>
          {items.map((item, index) => (
            <li key={keyExtractor(item)}>
              {renderItem(item, index)}
            </li>
          ))}
        </ul>
      );
    }
    

5.1.4. Styling Strategy

  1. CSS-in-JS with Theme Support

    const StyledButton = styled.button<ButtonProps>`
      padding: ${({ size, theme }) => theme.spacing[size]};
      background-color: ${({ variant, theme }) => theme.colors[variant]};
      border: none;
      border-radius: ${({ theme }) => theme.borderRadius.medium};
      
      &:disabled {
        opacity: 0.6;
        cursor: not-allowed;
      }
    `;
    
  2. CSS Custom Properties for Theming

    :root {
      --color-primary: #007bff;
      --color-secondary: #6c757d;
      --spacing-small: 8px;
      --spacing-medium: 16px;
    }
    

5.1.5. Documentation & Testing

  1. Storybook Integration

    • Document all component variants
    • Provide interactive examples
    • Include accessibility testing
  2. Comprehensive Testing

    describe('Button Component', () => {
      it('renders with correct accessibility attributes', () => {
        render(<Button aria-label="Test button">Click me</Button>);
        const button = screen.getByRole('button', { name: 'Test button' });
        expect(button).toBeInTheDocument();
      });
      
      it('handles keyboard navigation', () => {
        const handleClick = jest.fn();
        render(<Button onClick={handleClick}>Click me</Button>);
        const button = screen.getByRole('button');
        
        fireEvent.keyDown(button, { key: 'Enter' });
        expect(handleClick).toHaveBeenCalled();
      });
    });
    

5.1.6. Performance Considerations

  1. Memoization

    const ExpensiveComponent = React.memo<Props>(({ data, onAction }) => {
      const processedData = useMemo(() => {
        return data.map(item => expensiveTransformation(item));
      }, [data]);
      
      return <div>{/* Render processed data */}</div>;
    });
    
  2. Lazy Loading

    const LazyModal = React.lazy(() => import('./Modal'));
    
    const App = () => (
      <Suspense fallback={<Spinner />}>
        <LazyModal />
      </Suspense>
    );
    

This approach ensures components are maintainable, accessible, performant, and provide a great developer experience.

Explain different strategies for code splitting in React. How would you implement route-based code splitting?

Answer:

Code splitting is a technique to split your code into smaller chunks that can be loaded on demand, improving initial load performance.

5.2.1. Code Splitting Strategies

  1. Route-Based Code Splitting

    import { lazy, Suspense } from 'react';
    import { BrowserRouter, Routes, Route } from 'react-router-dom';
    
    // Lazy load route components
    const Home = lazy(() => import('./pages/Home'));
    const About = lazy(() => import('./pages/About'));
    const Contact = lazy(() => import('./pages/Contact'));
    const Dashboard = lazy(() => import('./pages/Dashboard'));
    
    const App = () => (
      <BrowserRouter>
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<Contact />} />
            <Route path="/dashboard" element={<Dashboard />} />
          </Routes>
        </Suspense>
      </BrowserRouter>
    );
    
  2. Component-Based Code Splitting

    import { lazy, Suspense, useState } from 'react';
    
    const HeavyChart = lazy(() => import('./HeavyChart'));
    const DataTable = lazy(() => import('./DataTable'));
    
    const Dashboard = () => {
      const [showChart, setShowChart] = useState(false);
      const [showTable, setShowTable] = useState(false);
    
      return (
        <div>
          <button onClick={() => setShowChart(true)}>
            Load Chart
          </button>
          <button onClick={() => setShowTable(true)}>
            Load Table
          </button>
    
          {showChart && (
            <Suspense fallback={<div>Loading chart...</div>}>
              <HeavyChart />
            </Suspense>
          )}
    
          {showTable && (
            <Suspense fallback={<div>Loading table...</div>}>
              <DataTable />
            </Suspense>
          )}
        </div>
      );
    };
    
  3. Library-Based Code Splitting

    // Split large libraries
    const loadMoment = () => import('moment');
    const loadLodash = () => import('lodash');
    
    const DateFormatter = () => {
      const [moment, setMoment] = useState(null);
    
      useEffect(() => {
        loadMoment().then(({ default: moment }) => {
          setMoment(moment);
        });
      }, []);
    
      if (!moment) return <div>Loading date formatter...</div>;
    
      return <div>{moment().format('YYYY-MM-DD')}</div>;
    };
    

5.2.2. Advanced Code Splitting Patterns

  1. Preloading with Intersection Observer

    const usePreloadOnIntersection = (importFn: () => Promise<any>) => {
      const [isLoaded, setIsLoaded] = useState(false);
      const ref = useRef<HTMLDivElement>(null);
    
      useEffect(() => {
        const observer = new IntersectionObserver(
          ([entry]) => {
            if (entry.isIntersecting && !isLoaded) {
              importFn().then(() => setIsLoaded(true));
              observer.disconnect();
            }
          },
          { threshold: 0.1 }
        );
    
        if (ref.current) {
          observer.observe(ref.current);
        }
    
        return () => observer.disconnect();
      }, [importFn, isLoaded]);
    
      return { ref, isLoaded };
    };
    
    const LazyComponent = () => {
      const { ref, isLoaded } = usePreloadOnIntersection(
        () => import('./HeavyComponent')
      );
    
      return (
        <div ref={ref}>
          {isLoaded ? (
            <Suspense fallback={<div>Loading...</div>}>
              <HeavyComponent />
            </Suspense>
          ) : (
            <div>Component will load when visible</div>
          )}
        </div>
      );
    };
    
  2. Dynamic Imports with Error Boundaries

    class ChunkLoadErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false, retryCount: 0 };
      }
    
      static getDerivedStateFromError(error) {
        if (error.name === 'ChunkLoadError') {
          return { hasError: true };
        }
        return null;
      }
    
      componentDidCatch(error, errorInfo) {
        if (error.name === 'ChunkLoadError') {
          console.error('Chunk load failed:', error);
        }
      }
    
      retry = () => {
        this.setState(prevState => ({
          hasError: false,
          retryCount: prevState.retryCount + 1
        }));
      };
    
      render() {
        if (this.state.hasError) {
          return (
            <div>
              <h2>Failed to load component</h2>
              <button onClick={this.retry}>
                Retry (Attempt {this.state.retryCount + 1})
              </button>
            </div>
          );
        }
    
        return this.props.children;
      }
    }
    
    const App = () => (
      <ChunkLoadErrorBoundary>
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/dashboard" element={<Dashboard />} />
          </Routes>
        </Suspense>
      </ChunkLoadErrorBoundary>
    );
    
  3. Conditional Code Splitting

    const ConditionalComponent = ({ userRole }: { userRole: string }) => {
      const [AdminPanel, setAdminPanel] = useState<React.ComponentType | null>(null);
    
      useEffect(() => {
        if (userRole === 'admin') {
          import('./AdminPanel').then(({ default: AdminPanelComponent }) => {
            setAdminPanel(() => AdminPanelComponent);
          });
        }
      }, [userRole]);
    
      if (userRole === 'admin' && AdminPanel) {
        return (
          <Suspense fallback={<div>Loading admin panel...</div>}>
            <AdminPanel />
          </Suspense>
        );
      }
    
      return <div>Regular user content</div>;
    };
    

5.2.3. Webpack Configuration for Code Splitting

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },
};

5.2.4. Performance Monitoring

const useChunkLoadTime = () => {
  const [loadTimes, setLoadTimes] = useState<Record<string, number>>({});
  
  const trackChunkLoad = (chunkName: string) => {
    const startTime = performance.now();
    
    return () => {
      const endTime = performance.now();
      const loadTime = endTime - startTime;
      
      setLoadTimes(prev => ({
        ...prev,
        [chunkName]: loadTime
      }));
      
      // Send to analytics
      analytics.track('chunk_loaded', {
        chunk: chunkName,
        loadTime,
        timestamp: Date.now()
      });
    };
  };
  
  return { loadTimes, trackChunkLoad };
};

// Usage
const Dashboard = () => {
  const { trackChunkLoad } = useChunkLoadTime();
  
  useEffect(() => {
    const endTracking = trackChunkLoad('dashboard');
    return endTracking;
  }, [trackChunkLoad]);
  
  return <div>Dashboard content</div>;
};

5.2.5. Best Practices

  1. Bundle Analysis

    # Analyze bundle size
    npm install --save-dev webpack-bundle-analyzer
    npx webpack-bundle-analyzer build/static/js/*.js
    
  2. Loading States

    • Always provide meaningful loading states
    • Consider skeleton screens for better UX
    • Implement retry mechanisms for failed chunks
  3. Preloading Strategy

    • Preload critical routes on user interaction
    • Use <link rel="prefetch"> for likely next pages
    • Implement intelligent preloading based on user behavior
  4. Error Handling

    • Wrap lazy components in error boundaries
    • Provide fallback UI for chunk load failures
    • Implement retry mechanisms

This comprehensive approach ensures optimal performance while maintaining a great user experience.

What's your approach to testing React applications? How do you balance unit, integration, and e2e tests?

Answer:

A comprehensive testing strategy for React applications follows the testing pyramid principle, balancing different types of tests for optimal coverage and maintainability.

5.3.1. Testing Pyramid Structure

  1. Unit Tests (70%) - Fast, isolated tests for individual functions/components
  2. Integration Tests (20%) - Test component interactions and data flow
  3. End-to-End Tests (10%) - Full user journey testing

5.3.2. Unit Testing

Component Testing with React Testing Library

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button Component', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('handles click events', async () => {
    const handleClick = jest.fn();
    const user = userEvent.setup();
    
    render(<Button onClick={handleClick}>Click me</Button>);
    
    await user.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('shows loading state', () => {
    render(<Button loading>Click me</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });
});

Custom Hook Testing

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter Hook', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());
    expect(result.current.count).toBe(0);
  });

  it('increments counter', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
  });

  it('resets counter', () => {
    const { result } = renderHook(() => useCounter());
    
    act(() => {
      result.current.increment();
      result.current.reset();
    });
    
    expect(result.current.count).toBe(0);
  });
});

Utility Function Testing

import { formatCurrency, validateEmail } from './utils';

describe('Utility Functions', () => {
  describe('formatCurrency', () => {
    it('formats positive numbers correctly', () => {
      expect(formatCurrency(1234.56)).toBe('$1,234.56');
    });

    it('handles zero', () => {
      expect(formatCurrency(0)).toBe('$0.00');
    });

    it('handles negative numbers', () => {
      expect(formatCurrency(-100)).toBe('-$100.00');
    });
  });

  describe('validateEmail', () => {
    it('validates correct email formats', () => {
      expect(validateEmail('test@example.com')).toBe(true);
      expect(validateEmail('user.name@domain.co.uk')).toBe(true);
    });

    it('rejects invalid email formats', () => {
      expect(validateEmail('invalid-email')).toBe(false);
      expect(validateEmail('@domain.com')).toBe(false);
      expect(validateEmail('user@')).toBe(false);
    });
  });
});

5.3.3. Integration Testing

Component Integration with Context

import { render, screen, waitFor } from '@testing-library/react';
import { ThemeProvider } from './ThemeProvider';
import { ThemedButton } from './ThemedButton';

const renderWithTheme = (component: React.ReactElement) => {
  return render(
    <ThemeProvider theme="dark">
      {component}
    </ThemeProvider>
  );
};

describe('ThemedButton Integration', () => {
  it('applies theme styles correctly', () => {
    renderWithTheme(<ThemedButton>Click me</ThemedButton>);
    
    const button = screen.getByRole('button');
    expect(button).toHaveClass('dark-theme');
  });
});

API Integration Testing

import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserList } from './UserList';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
      ])
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('UserList Integration', () => {
  it('fetches and displays users', async () => {
    render(<UserList />);
    
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
      expect(screen.getByText('Jane Smith')).toBeInTheDocument();
    });
  });

  it('handles API errors gracefully', async () => {
    server.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );

    render(<UserList />);
    
    await waitFor(() => {
      expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
    });
  });
});

Form Integration Testing

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ContactForm } from './ContactForm';

describe('ContactForm Integration', () => {
  it('submits form with valid data', async () => {
    const user = userEvent.setup();
    const onSubmit = jest.fn();
    
    render(<ContactForm onSubmit={onSubmit} />);
    
    await user.type(screen.getByLabelText(/name/i), 'John Doe');
    await user.type(screen.getByLabelText(/email/i), 'john@example.com');
    await user.type(screen.getByLabelText(/message/i), 'Hello world');
    
    await user.click(screen.getByRole('button', { name: /submit/i }));
    
    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        name: 'John Doe',
        email: 'john@example.com',
        message: 'Hello world'
      });
    });
  });

  it('shows validation errors', async () => {
    const user = userEvent.setup();
    
    render(<ContactForm onSubmit={jest.fn()} />);
    
    await user.click(screen.getByRole('button', { name: /submit/i }));
    
    await waitFor(() => {
      expect(screen.getByText(/name is required/i)).toBeInTheDocument();
      expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    });
  });
});

5.3.4. End-to-End Testing

Playwright E2E Tests

import { test, expect } from '@playwright/test';

test.describe('User Authentication Flow', () => {
  test('user can login and access dashboard', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');
    
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
  });

  test('user can logout', async ({ page }) => {
    // Login first
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');
    
    // Logout
    await page.click('[data-testid="user-menu"]');
    await page.click('[data-testid="logout-button"]');
    
    await expect(page).toHaveURL('/login');
  });
});

test.describe('Shopping Cart Flow', () => {
  test('user can add items to cart and checkout', async ({ page }) => {
    await page.goto('/products');
    
    // Add first product
    await page.click('[data-testid="product-1"] [data-testid="add-to-cart"]');
    await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
    
    // Add second product
    await page.click('[data-testid="product-2"] [data-testid="add-to-cart"]');
    await expect(page.locator('[data-testid="cart-count"]')).toHaveText('2');
    
    // Go to cart
    await page.click('[data-testid="cart-icon"]');
    await expect(page).toHaveURL('/cart');
    
    // Proceed to checkout
    await page.click('[data-testid="checkout-button"]');
    await expect(page).toHaveURL('/checkout');
  });
});

5.3.5. Testing Configuration

Jest Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.tsx',
    '!src/setupTests.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Test Setup

// src/setupTests.ts
import '@testing-library/jest-dom';
import { server } from './mocks/server';

// Establish API mocking before all tests
beforeAll(() => server.listen());

// Reset any request handlers that we may add during the tests
afterEach(() => server.resetHandlers());

// Clean up after the tests are finished
afterAll(() => server.close());

// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
  constructor() {}
  disconnect() {}
  observe() {}
  unobserve() {}
};

5.3.6. Testing Best Practices

  1. Test Behavior, Not Implementation

    // Good: Tests user behavior
    it('shows error message when form is invalid', () => {
      render(<ContactForm />);
      fireEvent.click(screen.getByRole('button'));
      expect(screen.getByText(/please fill all fields/i)).toBeInTheDocument();
    });
    
    // Avoid: Tests implementation details
    it('calls validateForm when submit button is clicked', () => {
      const validateForm = jest.fn();
      render(<ContactForm validateForm={validateForm} />);
      fireEvent.click(screen.getByRole('button'));
      expect(validateForm).toHaveBeenCalled();
    });
    
  2. Use Data Test IDs Sparingly

    // Use semantic queries first
    screen.getByRole('button', { name: /submit/i });
    screen.getByLabelText(/email address/i);
    
    // Use data-testid only when necessary
    screen.getByTestId('complex-component');
    
  3. Mock External Dependencies

    // Mock API calls
    jest.mock('./api', () => ({
     fetchUsers: jest.fn(() => Promise.resolve(mockUsers)),
    

}));

// Mock browser APIs Object.defineProperty(window, 'localStorage', { value: { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), }, });


4. **Test Accessibility**
 ```tsx
 import { axe, toHaveNoViolations } from 'jest-axe';
 
 expect.extend(toHaveNoViolations);
 
 it('should not have accessibility violations', async () => {
   const { container } = render(<MyComponent />);
   const results = await axe(container);
   expect(results).toHaveNoViolations();
 });

5.3.7. Continuous Integration

GitHub Actions Workflow

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: npm run test:unit
      
      - name: Run integration tests
        run: npm run test:integration
      
      - name: Run E2E tests
        run: npm run test:e2e
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

This comprehensive testing strategy ensures code quality, catches regressions early, and provides confidence when deploying changes to production.

How would you implement authentication in a React SPA? Discuss token management, refresh strategies, and protected routes.

Answer:

Implementing authentication in a React SPA requires careful consideration of security, user experience, and token management. Here's a comprehensive approach:

5.4.1. Authentication Architecture

Auth Context and Provider

interface User {
  id: string;
  email: string;
  name: string;
  role: 'user' | 'admin';
  permissions: string[];
}

interface AuthState {
  user: User | null;
  token: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
}

interface AuthContextType extends AuthState {
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  refreshAuthToken: () => Promise<void>;
  hasPermission: (permission: string) => boolean;
  hasRole: (role: string) => boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [state, setState] = useState<AuthState>({
    user: null,
    token: null,
    refreshToken: null,
    isAuthenticated: false,
    isLoading: true,
    error: null,
  });

  // Initialize auth state from localStorage
  useEffect(() => {
    const initializeAuth = async () => {
      try {
        const token = localStorage.getItem('accessToken');
        const refreshToken = localStorage.getItem('refreshToken');
        
        if (token && refreshToken) {
          // Verify token and get user data
          const user = await verifyToken(token);
          setState(prev => ({
            ...prev,
            user,
            token,
            refreshToken,
            isAuthenticated: true,
            isLoading: false,
          }));
        } else {
          setState(prev => ({ ...prev, isLoading: false }));
        }
      } catch (error) {
        // Clear invalid tokens
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        setState(prev => ({ ...prev, isLoading: false }));
      }
    };

    initializeAuth();
  }, []);

  const login = async (email: string, password: string) => {
    setState(prev => ({ ...prev, isLoading: true, error: null }));
    
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        throw new Error('Invalid credentials');
      }

      const { user, accessToken, refreshToken } = await response.json();
      
      // Store tokens securely
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
      
      setState({
        user,
        token: accessToken,
        refreshToken,
        isAuthenticated: true,
        isLoading: false,
        error: null,
      });
    } catch (error) {
      setState(prev => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Login failed',
      }));
      throw error;
    }
  };

  const logout = () => {
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
    setState({
      user: null,
      token: null,
      refreshToken: null,
      isAuthenticated: false,
      isLoading: false,
      error: null,
    });
  };

  const refreshAuthToken = async () => {
    const refreshToken = localStorage.getItem('refreshToken');
    if (!refreshToken) {
      logout();
      return;
    }

    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refreshToken }),
      });

      if (!response.ok) {
        throw new Error('Token refresh failed');
      }

      const { accessToken, refreshToken: newRefreshToken } = await response.json();
      
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', newRefreshToken);
      
      setState(prev => ({
        ...prev,
        token: accessToken,
        refreshToken: newRefreshToken,
      }));
    } catch (error) {
      logout();
      throw error;
    }
  };

  const hasPermission = (permission: string): boolean => {
    return state.user?.permissions.includes(permission) ?? false;
  };

  const hasRole = (role: string): boolean => {
    return state.user?.role === role;
  };

  return (
    <AuthContext.Provider
      value={{
        ...state,
        login,
        logout,
        refreshAuthToken,
        hasPermission,
        hasRole,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

5.4.2. Token Management

HTTP Client with Automatic Token Refresh

class ApiClient {
  private baseURL: string;
  private refreshPromise: Promise<string> | null = null;

  constructor(baseURL: string) {
    this.baseURL = baseURL;
  }

  private async makeRequest<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const token = localStorage.getItem('accessToken');
    
    const config: RequestInit = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(token && { Authorization: `Bearer ${token}` }),
        ...options.headers,
      },
    };

    let response = await fetch(`${this.baseURL}${endpoint}`, config);

    // Handle token expiration
    if (response.status === 401 && token) {
      try {
        const newToken = await this.refreshToken();
        config.headers = {
          ...config.headers,
          Authorization: `Bearer ${newToken}`,
        };
        response = await fetch(`${this.baseURL}${endpoint}`, config);
      } catch (error) {
        // Redirect to login
        window.location.href = '/login';
        throw error;
      }
    }

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    return response.json();
  }

  private async refreshToken(): Promise<string> {
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = this.performTokenRefresh();
    
    try {
      const newToken = await this.refreshPromise;
      return newToken;
    } finally {
      this.refreshPromise = null;
    }
  }

  private async performTokenRefresh(): Promise<string> {
    const refreshToken = localStorage.getItem('refreshToken');
    if (!refreshToken) {
      throw new Error('No refresh token available');
    }

    const response = await fetch(`${this.baseURL}/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const { accessToken, refreshToken: newRefreshToken } = await response.json();
    
    localStorage.setItem('accessToken', accessToken);
    localStorage.setItem('refreshToken', newRefreshToken);
    
    return accessToken;
  }

  // Public methods
  async get<T>(endpoint: string): Promise<T> {
    return this.makeRequest<T>(endpoint, { method: 'GET' });
  }

  async post<T>(endpoint: string, data: any): Promise<T> {
    return this.makeRequest<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async put<T>(endpoint: string, data: any): Promise<T> {
    return this.makeRequest<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async delete<T>(endpoint: string): Promise<T> {
    return this.makeRequest<T>(endpoint, { method: 'DELETE' });
  }
}

export const apiClient = new ApiClient(process.env.REACT_APP_API_URL || '');

5.4.3. Protected Routes

Route Protection Components

interface ProtectedRouteProps {
  children: React.ReactNode;
  requiredPermission?: string;
  requiredRole?: string;
  fallback?: React.ReactNode;
}

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
  children,
  requiredPermission,
  requiredRole,
  fallback = <Navigate to="/login" replace />,
}) => {
  const { isAuthenticated, user, isLoading } = useAuth();

  if (isLoading) {
    return <LoadingSpinner />;
  }

  if (!isAuthenticated || !user) {
    return <>{fallback}</>;
  }

  if (requiredRole && !user.role.includes(requiredRole)) {
    return <Navigate to="/unauthorized" replace />;
  }

  if (requiredPermission && !user.permissions.includes(requiredPermission)) {
    return <Navigate to="/unauthorized" replace />;
  }

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

// Usage in routing
const AppRoutes = () => {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route path="/register" element={<RegisterPage />} />
      
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />
      
      <Route
        path="/admin"
        element={
          <ProtectedRoute requiredRole="admin">
            <AdminPanel />
          </ProtectedRoute>
        }
      />
      
      <Route
        path="/settings"
        element={
          <ProtectedRoute requiredPermission="manage_settings">
            <SettingsPage />
          </ProtectedRoute>
        }
      />
      
      <Route path="/unauthorized" element={<UnauthorizedPage />} />
      <Route path="*" element={<NotFoundPage />} />
    </Routes>
  );
};

5.4.4. Advanced Security Features

Session Management

const useSessionManagement = () => {
  const { logout } = useAuth();
  const [lastActivity, setLastActivity] = useState(Date.now());
  const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes

  useEffect(() => {
    const updateActivity = () => setLastActivity(Date.now());
    
    // Track user activity
    const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
    events.forEach(event => {
      document.addEventListener(event, updateActivity, true);
    });

    // Check for session timeout
    const checkSession = setInterval(() => {
      if (Date.now() - lastActivity > SESSION_TIMEOUT) {
        logout();
        clearInterval(checkSession);
      }
    }, 60000); // Check every minute

    return () => {
      events.forEach(event => {
        document.removeEventListener(event, updateActivity, true);
      });
      clearInterval(checkSession);
    };
  }, [lastActivity, logout]);

  return { lastActivity };
};

Multi-Factor Authentication

interface MFAState {
  isEnabled: boolean;
  backupCodes: string[];
  qrCode: string;
}

const useMFA = () => {
  const [mfaState, setMfaState] = useState<MFAState | null>(null);
  const { user } = useAuth();

  const enableMFA = async () => {
    try {
      const response = await apiClient.post('/auth/mfa/setup', {});
      setMfaState(response);
    } catch (error) {
      console.error('Failed to setup MFA:', error);
    }
  };

  const verifyMFA = async (token: string) => {
    try {
      await apiClient.post('/auth/mfa/verify', { token });
      return true;
    } catch (error) {
      return false;
    }
  };

  const disableMFA = async (password: string) => {
    try {
      await apiClient.post('/auth/mfa/disable', { password });
      setMfaState(null);
    } catch (error) {
      console.error('Failed to disable MFA:', error);
    }
  };

  return {
    mfaState,
    enableMFA,
    verifyMFA,
    disableMFA,
  };
};

5.4.5. Security Best Practices

Token Storage Security

// Secure token storage with encryption
class SecureStorage {
  private static encrypt(data: string): string {
    // Implement encryption logic
    return btoa(data); // Simple base64 for demo
  }

  private static decrypt(encryptedData: string): string {
    // Implement decryption logic
    return atob(encryptedData); // Simple base64 for demo
  }

  static setItem(key: string, value: string): void {
    const encrypted = this.encrypt(value);
    localStorage.setItem(key, encrypted);
  }

  static getItem(key: string): string | null {
    const encrypted = localStorage.getItem(key);
    if (!encrypted) return null;
    
    try {
      return this.decrypt(encrypted);
    } catch {
      return null;
    }
  }

  static removeItem(key: string): void {
    localStorage.removeItem(key);
  }
}

// Use secure storage for tokens
SecureStorage.setItem('accessToken', token);
const token = SecureStorage.getItem('accessToken');

CSRF Protection

const useCSRFProtection = () => {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);

  useEffect(() => {
    const fetchCSRFToken = async () => {
      try {
        const response = await fetch('/api/csrf-token');
        const { token } = await response.json();
        setCsrfToken(token);
      } catch (error) {
        console.error('Failed to fetch CSRF token:', error);
      }
    };

    fetchCSRFToken();
  }, []);

  const getHeaders = () => ({
    'X-CSRF-Token': csrfToken,
  });

  return { csrfToken, getHeaders };
};

5.4.6. Error Handling and User Experience

Auth Error Boundary

class AuthErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
    if (error.message.includes('401') || error.message.includes('403')) {
      return { hasError: true };
    }
    return null;
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    if (error.message.includes('401')) {
      // Redirect to login
      window.location.href = '/login';
    }
  }

  render() {
    if (this.state.hasError) {
      return <Navigate to="/login" replace />;
    }

    return this.props.children;
  }
}

This comprehensive authentication system provides secure, user-friendly authentication with proper token management, role-based access control, and advanced security features.

Design a robust data fetching layer for a React application. How would you handle caching, error states, and optimistic updates?

Answer:

A robust data fetching layer is crucial for modern React applications. Here's a comprehensive approach that handles caching, error states, optimistic updates, and more:

5.5.1. Core Data Fetching Architecture

Custom Hook for Data Fetching

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}

interface FetchOptions {
  enabled?: boolean;
  retry?: number;
  retryDelay?: number;
  staleTime?: number;
  cacheTime?: number;
  onSuccess?: (data: any) => void;
  onError?: (error: Error) => void;
}

const useFetch = <T>(
  url: string,
  options: FetchOptions = {}
): FetchState<T> => {
  const {
    enabled = true,
    retry = 3,
    retryDelay = 1000,
    staleTime = 5 * 60 * 1000, // 5 minutes
    cacheTime = 10 * 60 * 1000, // 10 minutes
    onSuccess,
    onError,
  } = options;

  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: false,
    error: null,
    refetch: () => Promise.resolve(),
  });

  const cache = useRef<Map<string, { data: T; timestamp: number }>>(new Map());

  const fetchData = useCallback(async (retryCount = 0): Promise<void> => {
    if (!enabled) return;

    // Check cache first
    const cached = cache.current.get(url);
    if (cached && Date.now() - cached.timestamp < staleTime) {
      setState(prev => ({ ...prev, data: cached.data, loading: false }));
      onSuccess?.(cached.data);
      return;
    }

    setState(prev => ({ ...prev, loading: true, error: null }));

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      
      // Cache the data
      cache.current.set(url, { data, timestamp: Date.now() });
      
      setState(prev => ({ ...prev, data, loading: false }));
      onSuccess?.(data);
    } catch (error) {
      const err = error instanceof Error ? error : new Error('Unknown error');
      
      if (retryCount < retry) {
        // Retry with exponential backoff
        setTimeout(() => {
          fetchData(retryCount + 1);
        }, retryDelay * Math.pow(2, retryCount));
      } else {
        setState(prev => ({ ...prev, error: err, loading: false }));
        onError?.(err);
      }
    }
  }, [url, enabled, retry, retryDelay, staleTime, onSuccess, onError]);

  const refetch = useCallback(() => {
    cache.current.delete(url);
    return fetchData();
  }, [fetchData, url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  // Cleanup old cache entries
  useEffect(() => {
    const cleanup = setInterval(() => {
      const now = Date.now();
      for (const [key, value] of cache.current.entries()) {
        if (now - value.timestamp > cacheTime) {
          cache.current.delete(key);
        }
      }
    }, 60000); // Cleanup every minute

    return () => clearInterval(cleanup);
  }, [cacheTime]);

  return { ...state, refetch };
};

5.5.2. Advanced Caching with React Query

React Query Setup

import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      retry: 3,
      retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
    mutations: {
      retry: 1,
    },
  },
});

// Custom hooks for different data types
export const useUsers = () => {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
    select: (data) => data.users,
  });
};

export const useUser = (id: string) => {
  return useQuery({
    queryKey: ['users', id],
    queryFn: () => fetch(`/api/users/${id}`).then(res => res.json()),
    enabled: !!id,
  });
};

export const useCreateUser = () => {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (userData: CreateUserData) =>
      fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      }).then(res => res.json()),
    onSuccess: (newUser) => {
      // Invalidate and refetch users list
      queryClient.invalidateQueries({ queryKey: ['users'] });
      
      // Optimistically update the cache
      queryClient.setQueryData(['users', newUser.id], newUser);
    },
    onError: (error) => {
      console.error('Failed to create user:', error);
    },
  });
};

5.5.3. Optimistic Updates

Optimistic Update Implementation

interface OptimisticUpdate<T> {
  queryKey: string[];
  updateFn: (oldData: T) => T;
  rollbackFn?: (oldData: T) => T;
}

const useOptimisticMutation = <T, TVariables>(
  mutationFn: (variables: TVariables) => Promise<T>,
  optimisticUpdate: OptimisticUpdate<T>
) => {
  const queryClient = useQueryClient();
  const [isOptimistic, setIsOptimistic] = useState(false);

  return useMutation({
    mutationFn,
    onMutate: async (variables) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: optimisticUpdate.queryKey });

      // Snapshot previous value
      const previousData = queryClient.getQueryData<T>(optimisticUpdate.queryKey);

      // Optimistically update
      if (previousData) {
        queryClient.setQueryData(
          optimisticUpdate.queryKey,
          optimisticUpdate.updateFn(previousData)
        );
        setIsOptimistic(true);
      }

      return { previousData };
    },
    onError: (err, variables, context) => {
      // Rollback on error
      if (context?.previousData && optimisticUpdate.rollbackFn) {
        queryClient.setQueryData(
          optimisticUpdate.queryKey,
          optimisticUpdate.rollbackFn(context.previousData)
        );
      }
      setIsOptimistic(false);
    },
    onSettled: () => {
      // Always refetch after error or success
      queryClient.invalidateQueries({ queryKey: optimisticUpdate.queryKey });
      setIsOptimistic(false);
    },
  });
};

// Usage example
const useToggleLike = () => {
  return useOptimisticMutation(
    (postId: string) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }).then(res => res.json()),
    {
      queryKey: ['posts'],
      updateFn: (oldData) => ({
        ...oldData,
        posts: oldData.posts.map(post => 
          post.id === postId 
            ? { ...post, liked: !post.liked, likes: post.liked ? post.likes - 1 : post.likes + 1 }
            : post
        ),
      }),
      rollbackFn: (oldData) => ({
        ...oldData,
        posts: oldData.posts.map(post => 
          post.id === postId 
            ? { ...post, liked: !post.liked, likes: post.liked ? post.likes + 1 : post.likes - 1 }
            : post
        ),
      }),
    }
  );
};

5.5.4. Error Handling and Retry Logic

Advanced Error Handling

interface ApiError extends Error {
  status?: number;
  code?: string;
  details?: any;
}

class ApiErrorHandler {
  static async handleResponse(response: Response): Promise<any> {
    if (!response.ok) {
      const error: ApiError = new Error(`HTTP ${response.status}: ${response.statusText}`);
      error.status = response.status;
      
      try {
        const errorData = await response.json();
        error.details = errorData;
        error.message = errorData.message || error.message;
      } catch {
        // If response is not JSON, use status text
      }
      
      throw error;
    }
    
    return response.json();
  }

  static isRetryableError(error: ApiError): boolean {
    if (!error.status) return true; // Network errors are retryable
    
    // Retry on server errors and rate limiting
    return error.status >= 500 || error.status === 429;
  }

  static getRetryDelay(attempt: number, baseDelay = 1000): number {
    // Exponential backoff with jitter
    const delay = baseDelay * Math.pow(2, attempt);
    const jitter = Math.random() * 0.1 * delay;
    return Math.min(delay + jitter, 30000); // Max 30 seconds
  }
}

const useApiCall = <T>(
  url: string,
  options: RequestInit = {},
  retryOptions = { maxRetries: 3, baseDelay: 1000 }
) => {
  const [state, setState] = useState<{
    data: T | null;
    loading: boolean;
    error: ApiError | null;
  }>({
    data: null,
    loading: false,
    error: null,
  });

  const execute = useCallback(async (retryCount = 0): Promise<T> => {
    setState(prev => ({ ...prev, loading: true, error: null }));

    try {
      const response = await fetch(url, options);
      const data = await ApiErrorHandler.handleResponse(response);
      
      setState({ data, loading: false, error: null });
      return data;
    } catch (error) {
      const apiError = error as ApiError;
      
      if (
        retryCount < retryOptions.maxRetries &&
        ApiErrorHandler.isRetryableError(apiError)
      ) {
        const delay = ApiErrorHandler.getRetryDelay(retryCount, retryOptions.baseDelay);
        
        await new Promise(resolve => setTimeout(resolve, delay));
        return execute(retryCount + 1);
      }
      
      setState({ data: null, loading: false, error: apiError });
      throw apiError;
    }
  }, [url, options, retryOptions]);

  return { ...state, execute };
};

5.5.5. Real-time Data with WebSockets

WebSocket Integration

interface WebSocketMessage {
  type: string;
  payload: any;
  timestamp: number;
}

const useWebSocket = (url: string, options: { reconnect?: boolean; reconnectInterval?: number } = {}) => {
  const { reconnect = true, reconnectInterval = 3000 } = options;
  const [socket, setSocket] = useState<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [lastMessage, setLastMessage] = useState<WebSocketMessage | null>(null);
  const [error, setError] = useState<Event | null>(null);

  const connect = useCallback(() => {
    const ws = new WebSocket(url);
    
    ws.onopen = () => {
      setIsConnected(true);
      setError(null);
    };
    
    ws.onmessage = (event) => {
      try {
        const message: WebSocketMessage = JSON.parse(event.data);
        setLastMessage(message);
      } catch (err) {
        console.error('Failed to parse WebSocket message:', err);
      }
    };
    
    ws.onclose = () => {
      setIsConnected(false);
      if (reconnect) {
        setTimeout(connect, reconnectInterval);
      }
    };
    
    ws.onerror = (event) => {
      setError(event);
    };
    
    setSocket(ws);
  }, [url, reconnect, reconnectInterval]);

  const sendMessage = useCallback((message: any) => {
    if (socket && isConnected) {
      socket.send(JSON.stringify(message));
    }
  }, [socket, isConnected]);

  const disconnect = useCallback(() => {
    if (socket) {
      socket.close();
      setSocket(null);
    }
  }, [socket]);

  useEffect(() => {
    connect();
    return disconnect;
  }, [connect, disconnect]);

  return {
    isConnected,
    lastMessage,
    error,
    sendMessage,
    disconnect,
    reconnect: connect,
  };
};

// Usage with React Query for real-time updates
const useRealtimePosts = () => {
  const queryClient = useQueryClient();
  
  const { lastMessage } = useWebSocket('ws://localhost:8080/posts');
  
  useEffect(() => {
    if (lastMessage) {
      switch (lastMessage.type) {
        case 'POST_CREATED':
          queryClient.invalidateQueries({ queryKey: ['posts'] });
          break;
        case 'POST_UPDATED':
          queryClient.setQueryData(['posts', lastMessage.payload.id], lastMessage.payload);
          break;
        case 'POST_DELETED':
          queryClient.setQueryData(['posts'], (old: any) => 
            old?.filter((post: any) => post.id !== lastMessage.payload.id)
          );
          break;
      }
    }
  }, [lastMessage, queryClient]);
  
  return useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(res => res.json()),
  });
};

5.5.6. Offline Support and Background Sync

Service Worker Integration

// service-worker.js
const CACHE_NAME = 'api-cache-v1';
const OFFLINE_QUEUE = 'offline-queue';

// Cache API responses
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      caches.match(event.request).then((response) => {
        if (response) {
          return response;
        }
        
        return fetch(event.request).then((response) => {
          if (response.status === 200) {
            const responseClone = response.clone();
            caches.open(CACHE_NAME).then((cache) => {
              cache.put(event.request, responseClone);
            });
          }
          return response;
        }).catch(() => {
          // Return cached version or queue for later
          return caches.match(event.request);
        });
      })
    );
  }
});

// Background sync for offline actions
self.addEventListener('sync', (event) => {
  if (event.tag === 'background-sync') {
    event.waitUntil(processOfflineQueue());
  }
});

const processOfflineQueue = async () => {
  const queue = await getOfflineQueue();
  
  for (const request of queue) {
    try {
      await fetch(request.url, request.options);
      await removeFromOfflineQueue(request.id);
    } catch (error) {
      console.error('Failed to sync request:', error);
    }
  }
};

Offline-Aware Data Fetching

const useOfflineAwareFetch = <T>(url: string, options: RequestInit = {}) => {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [offlineQueue, setOfflineQueue] = useState<any[]>([]);

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

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

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

  const executeRequest = useCallback(async (): Promise<T> => {
    if (!isOnline && options.method !== 'GET') {
      // Queue non-GET requests when offline
      const queuedRequest = {
        id: Date.now().toString(),
        url,
        options,
        timestamp: Date.now(),
      };
      
      setOfflineQueue(prev => [...prev, queuedRequest]);
      throw new Error('Request queued for offline sync');
    }

    const response = await fetch(url, options);
    return ApiErrorHandler.handleResponse(response);
  }, [url, options, isOnline]);

  return { executeRequest, isOnline, offlineQueue };
};

5.5.7. Performance Optimization

Request Deduplication

class RequestDeduplicator {
  private static pendingRequests = new Map<string, Promise<any>>();

  static async deduplicate<T>(key: string, requestFn: () => Promise<T>): Promise<T> {
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key)!;
    }

    const promise = requestFn().finally(() => {
      this.pendingRequests.delete(key);
    });

    this.pendingRequests.set(key, promise);
    return promise;
  }
}

const useDeduplicatedFetch = <T>(url: string) => {
  return useQuery({
    queryKey: [url],
    queryFn: () => RequestDeduplicator.deduplicate(url, () => 
      fetch(url).then(res => res.json())
    ),
  });
};

This comprehensive data fetching layer provides robust caching, error handling, optimistic updates, real-time capabilities, offline support, and performance optimizations for modern React applications.