Skip to main content

React Calculator Implementation with TypeScript and State Management

Comprehensive low-level design for a feature-rich calculator component with keyboard support, memory functions, and responsive UI.

This document presents a production-ready calculator implementation with modular architecture, proper state management, and accessibility considerations for enterprise frontend development.

1. Problem Statement & Requirements

Clarifying questions

  1. What is the target browser support matrix (e.g., IE11, mobile Safari, Chrome 80+)?
  2. Should this calculator support real-time calculations or only after pressing equals?
  3. Do we need to support keyboard input and accessibility features like screen readers?
  4. Should the calculator work offline or rely on network connectivity for all operations?
  5. Is there a specific design system or component library we should use (e.g., Material UI, Tailwind)?
  6. Do we need to support internationalization for different number formats or locales?
  7. Should this be a standalone calculator or integrated into a larger application?

Functional requirements

  • Basic arithmetic operations: addition (+), subtraction (-), multiplication (×), division (÷)
  • Decimal point support for floating-point numbers
  • Clear (C) and All Clear (AC) functionality
  • Backspace/delete character support
  • Percentage calculation (e.g., 50% of 100 = 50)
  • Equals (=) to compute results
  • Display of current input and calculation history
  • Handling of invalid operations (e.g., division by zero)

Non-functional requirements

  • Performance: All calculations must complete within 10ms for responsive UI
  • Accessibility: Full keyboard navigation and screen reader support (WCAG 2.1 AA)
  • Scalability: Support for future extension to scientific or financial functions
  • Usability: Responsive design that works on mobile, tablet, and desktop devices

Constraints and assumptions

  • Calculator will be implemented as a self-contained React component
  • No external state management library (e.g., Redux) required for basic functionality
  • Input validation will be handled client-side only
  • No external API calls or real-time data dependencies
  • UI will use standard HTML elements with semantic structure

Out of scope

  • Scientific or trigonometric functions (e.g., sin, cos, log)
  • Memory functions (M+, M-, MR, MC)
  • History tracking or persistent storage of calculation history
  • Advanced formatting (e.g., comma-separated thousands)
  • Multi-step calculations with parentheses support
  • Dark mode or theming beyond basic color contrast requirements

2. Component Architecture

graph TD
    A[Calculator] --> B[Display]
    A --> C[ButtonGrid]
    C --> D[Button]
    C --> E[OperatorButton]
    C --> F[EqualsButton]
    C --> G[ClearButton]
    C --> H[DecimalButton]
    A --> I[HistoryPanel]
    I --> J[HistoryItem]

Component Tree Breakdown

Smart Components (Container):

  • Calculator - Main container managing state and business logic
  • Display - Renders current input and calculation results
  • ButtonGrid - Organizes and renders calculator buttons

Dumb Components (Presentational):

  • Button - Generic button component with variant styling
  • OperatorButton - Specialized operator button (e.g., +, -, ×, ÷)
  • EqualsButton - Specialized equals button with distinct styling
  • ClearButton - Clear functionality button
  • DecimalButton - Decimal point button
  • HistoryPanel - Displays calculation history
  • HistoryItem - Individual history entry

TypeScript Props Contracts

// Calculator Component
interface CalculatorProps {
  initialDisplayValue?: string;
  historyLimit?: number;
}

// Display Component
interface DisplayProps {
  value: string;
  error?: boolean;
  isHistoryVisible: boolean;
}

// Button Component
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'number' | 'operator' | 'equals' | 'clear' | 'decimal';
  disabled?: boolean;
  ariaLabel?: string;
}

// HistoryPanel Component
interface HistoryPanelProps {
  history: HistoryItem[];
  onClearHistory: () => void;
  isVisible: boolean;
}

// HistoryItem Component
interface HistoryItem {
  id: string;
  expression: string;
  result: string;
  timestamp: Date;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Inputting : digit/operator/decimal
    Inputting --> Inputting : digit/operator/decimal
    Inputting --> Calculating : equals
    Inputting --> Error : invalid_operation
    Calculating --> Idle : success
    Calculating --> Error : division_by_zero
    Error --> Idle : clear
    Error --> Inputting : continue_calculation
    Idle --> HistoryVisible : toggle_history
    HistoryVisible --> Idle : close_history

Component Responsibilities

  • Calculator: Manages overall state, handles calculation logic, maintains history
  • Display: Renders current input and results with appropriate formatting
  • ButtonGrid: Organizes button layout, handles button press events
  • Button: Reusable base component for all calculator buttons with variant styling
  • HistoryPanel: Displays historical calculations, supports clearing history

Reusability Decisions

The calculator components are designed to be highly reusable:

  • Button component is generic and can be used in other UI contexts
  • Display can be reused for other numeric input scenarios
  • History components can be extracted for use in other calculation-based features

Component Lifecycle Considerations

  • Calculator component mounts and initializes state
  • Display updates on every calculation step
  • Button components handle click events with proper accessibility attributes
  • History panel lifecycle tied to visibility state and user interaction

The component architecture follows the principle of separating concerns while maintaining clear data flow from user input to calculation results, with proper error handling and accessibility support throughout.

3. State Management Design

Local state vs. global state decisions

For a calculator feature, most state can be managed locally within the component hierarchy since it's a self-contained UI with minimal cross-feature dependencies. The core calculation state (current input, operation, history) is naturally scoped to the calculator component and its children.

State management approach

React Context API + Zustand for complex state logic

  • Context API: Used for simple shared state like theme preferences or user settings that might be needed across multiple calculator instances
  • Zustand: Primary state management solution for the calculator's core logic due to:
    • Minimal boilerplate and clean API
    • Excellent TypeScript support with type inference
    • Easy to test and mock
    • No need for Redux store setup or middleware
    • Good performance characteristics for simple state trees

State shape and normalization strategy

interface CalculatorState {
  currentValue: string;
  previousValue: string | null;
  operation: string | null;
  waitingForOperand: boolean;
  history: CalculatorHistoryItem[];
}

interface CalculatorHistoryItem {
  expression: string;
  result: string;
  timestamp: number;
}

Server state vs. client state separation

Since this is a client-side calculator, all state is client-side. No server state is involved in basic calculation functionality.

Cache invalidation strategy

Not applicable for calculator - no caching or server-side state to invalidate.

Optimistic updates design

Not directly applicable since calculations are immediate and deterministic. However, we can implement optimistic UI updates for:

  • Immediate display of button presses
  • Smooth transitions between states (e.g., when clearing or entering new numbers)

Critical-path hook implementation

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CalculatorState {
  currentValue: string;
  previousValue: string | null;
  operation: string | null;
  waitingForOperand: boolean;
  history: CalculatorHistoryItem[];
  clear: () => void;
  inputDigit: (digit: string) => void;
  inputDecimal: () => void;
  toggleSign: () => void;
  inputPercent: () => void;
  performOperation: (nextOperation: string) => void;
  calculateResult: () => void;
  addToHistory: (expression: string, result: string) => void;
}

interface CalculatorHistoryItem {
  expression: string;
  result: string;
  timestamp: number;
}

const useCalculatorStore = create<CalculatorState>()(
  persist(
    (set, get) => ({
      currentValue: '0',
      previousValue: null,
      operation: null,
      waitingForOperand: false,
      history: [],
      
      clear: () => {
        set({
          currentValue: '0',
          previousValue: null,
          operation: null,
          waitingForOperand: false
        });
      },
      
      inputDigit: (digit) => {
        const { currentValue, waitingForOperand } = get();
        
        if (waitingForOperand) {
          set({
            currentValue: digit,
            waitingForOperand: false
          });
        } else {
          set({
            currentValue: currentValue === '0' ? digit : currentValue + digit
          });
        }
      },
      
      inputDecimal: () => {
        const { currentValue, waitingForOperand } = get();
        
        if (waitingForOperand) {
          set({
            currentValue: '0.',
            waitingForOperand: false
          });
        } else if (!currentValue.includes('.')) {
          set({
            currentValue: currentValue + '.'
          });
        }
      },
      
      toggleSign: () => {
        const { currentValue } = get();
        
        if (currentValue !== '0') {
          set({
            currentValue: currentValue.startsWith('-') 
              ? currentValue.slice(1) 
              : '-' + currentValue
          });
        }
      },
      
      inputPercent: () => {
        const { currentValue } = get();
        const value = parseFloat(currentValue);
        
        if (!isNaN(value)) {
          set({
            currentValue: String(value / 100)
          });
        }
      },
      
      performOperation: (nextOperation) => {
        const { currentValue, previousValue, operation } = get();
        
        if (previousValue === null) {
          set({
            previousValue: currentValue,
            operation: nextOperation,
            waitingForOperand: true
          });
        } else if (operation) {
          const result = calculate(
            parseFloat(previousValue),
            parseFloat(currentValue),
            operation
          );
          
          set({
            currentValue: String(result),
            previousValue: String(result),
            operation: nextOperation,
            waitingForOperand: true
          });
        }
      },
      
      calculateResult: () => {
        const { currentValue, previousValue, operation } = get();
        
        if (previousValue && operation) {
          const result = calculate(
            parseFloat(previousValue),
            
            parseFloat(currentValue),
            operation
          );
          
          const expression = `${previousValue} ${operation} ${currentValue}`;
          
          set({
            currentValue: String(result),
            previousValue: null,
            operation: null,
            waitingForOperand: true
          });
          
          // Add to history
          get().addToHistory(expression, String(result));
        }
      },
      
      addToHistory: (expression, result) => {
        set((state) => ({
          history: [
            ...state.history.slice(-9), // Keep only last 10 items
            {
              expression,
              result,
              timestamp: Date.now()
            }
          ]
        }));
      }
    }),
    {
      name: 'calculator-storage',
      partialize: (state) => ({
        history: state.history
      })
    }
  )
);

// Helper function for calculations
function calculate(
  first: number,
  second: number,
  operation: string
): number {
  switch (operation) {
    case '+':
      return first + second;
    case '-':
      return first - second;
    case '×':
      return first * second;
    case '÷':
      return first / second;
    default:
      return second;
  }
}

// Custom hook for calculator operations
export const useCalculator = () => {
  const {
    currentValue,
    previousValue,
    operation,
    waitingForOperand,
    history,
    clear,
    inputDigit,
    inputDecimal,
    toggleSign,
    inputPercent,
    performOperation,
    calculateResult
  } = useCalculatorStore();

  return {
    currentValue,
    previousValue,
    operation,
    waitingForOperand,
    history,
    clear,
    inputDigit,
    inputDecimal,
    toggleSign,
    inputPercent,
    performOperation,
    calculateResult
  };
};

4. Routing & Navigation

Route Structure and Hierarchy

The calculator feature will be a standalone application with a simple route structure:

/
  /calculator
    /history
    /memory

The calculator will be the main application route, with optional sub-routes for history and memory functions. All routes will be protected by authentication in a real application context.

Code Splitting Strategy

Route-based code splitting will be implemented using React.lazy and Suspense for the calculator main component:

// routes.tsx
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';

const Calculator = lazy(() => import('../components/Calculator'));
const History = lazy(() => import('../components/History'));
const Memory = lazy(() => import('../components/Memory'));

export const calculatorRoutes: RouteObject[] = [
  {
    path: '/calculator',
    element: <Calculator />,
    children: [
      {
        index: true,
        element: <Calculator />
      },
      {
        path: 'history',
        element: <History />
      },
      {
        path: 'memory',
        element: <Memory />
      }
    ]
  }
];

Component-based code splitting will be used for calculator sub-components:

  • Calculator display component
  • Keypad component (with button grouping)
  • History panel component

In a real application, authentication guards would be implemented using React Router's useNavigate hook and protected route wrappers:

// ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
  const { isAuthenticated, loading } = useAuth();
  
  if (loading) return <div>Loading...</div>;
  if (!isAuthenticated) return <Navigate to="/login" replace />;
  
  return children;
};

Deep Linking Support and URL Parameter Design

URL parameters will be minimal for this feature:

  • /calculator - Main calculator view
  • /calculator/history - History view with optional query parameters for filtering
  • /calculator/memory - Memory view

Example history filter URL: /calculator/history?date=2023-12-01&operation=add

Browser History Management and Scroll Restoration

React Router's built-in scroll restoration will be used:

// App.tsx
import { BrowserRouter, useScrollRestoration } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <ScrollRestoration />
      <Routes>
        {/* routes */}
      </Routes>
    </BrowserRouter>
  );
}

// Custom hook for scroll restoration
const ScrollRestoration = () => {
  useScrollRestoration();
  return null;
};

The calculator will maintain scroll position when navigating between history and memory views, and will reset to top when returning to the main calculator view.

  1. Tab-based navigation for history and memory views within the calculator context
  2. Stack-based navigation for back/forward functionality using browser history
  3. Keyboard navigation support for accessibility compliance (Tab, Enter, Escape keys)

Route Configuration

// router.tsx
import { createBrowserRouter } from 'react-router-dom';
import { ProtectedRoute } from './components/ProtectedRoute';

const router = createBrowserRouter([
  {
    path: '/',
    element: <ProtectedRoute><Calculator /></ProtectedRoute>,
    children: [
      {
        index: true,
        element: <Calculator />
      },
      {
        path: 'history',
        element: <ProtectedRoute><History /></ProtectedRoute>
      },
      {
        path: 'memory',
        element: <ProtectedRoute><Memory /></ProtectedRoute>
      }
    ]
  }
]);

export default router;

Performance Considerations

  • Route preloading for frequently accessed calculator sub-routes
  • Lazy loading of heavy components (history view with large datasets)
  • Route caching to prevent unnecessary re-renders during navigation
  • Memory cleanup of route-specific state when navigating away from views

Future Extensibility

The routing structure is designed to support:

  • Calculator themes (dark/light mode routes)
  • Advanced calculator modes (scientific, programmer)
  • User preferences persistence across navigation
  • Multi-tab calculator support with route-based state isolation

The current design provides a clean foundation that can be extended to support more complex calculator features while maintaining good performance and user experience.

5. API Integration Layer

API Client Design Pattern

For a calculator feature, we don't actually require external API calls since it's a client-side calculation tool. However, if this feature were to integrate with external services (e.g., storing calculation history, syncing across devices, or fetching currency conversion rates), we'd implement a centralized API client.

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

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
    timeout: 10000,
    headers: {
      'Content-Type': 'application/json',
    },
  });

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

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

  return client;
};

export const apiClient = createApiClient();

Request/Response Interceptors

  • Auth Token Injection: Automatically includes JWT token in all requests
  • Error Normalization: Transforms API errors into consistent error objects
  • Retry Strategy: Implements exponential backoff for transient network errors

Error Handling and Retry Strategy

// api/errorHandler.ts
export class ApiError extends Error {
  constructor(
    public status: number,
    public message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const handleApiError = (error: any): ApiError => {
  if (error.response) {
    return new ApiError(
      error.response.status,
      error.response.data?.message || 'API Error',
      error.response.data?.code
    );
  }
  
  if (error.request) {
    return new ApiError(503, 'Network Error - Please check your connection');
  }
  
  return new ApiError(500, 'Unknown Error occurred');
};

Loading, Error, and Empty States Design

// components/Calculator.tsx
interface CalculatorState {
  isLoading: boolean;
  error: string | null;
  calculations: Calculation[];
}

const Calculator = () => {
  const [state, setState] = useState<CalculatorState>({
    isLoading: false,
    error: null,
    calculations: [],
  });

  const fetchCalculations = async () => {
    try {
      setState(prev => ({ ...prev, isLoading: true }));
      const response = await apiClient.get<Calculation[]>('/calculations');
      setState(prev => ({
        ...prev,
        isLoading: false,
        calculations: response.data,
        error: null
      }));
    } catch (error) {
      const apiError = handleApiError(error);
      setState(prev => ({
        ...prev,
        isLoading: false,
        error: apiError.message
      }));
    }
  };
};

Data Fetching Strategy

For a calculator feature, we primarily use client-side rendering (CSR) since calculations happen locally. However, if calculation history needs to be persisted:

  • Primary Route: CSR for immediate calculation responses
  • History Route: SSR or SSG with pre-fetched data for better LCP

API Response Transformation Layer

// api/mappers.ts
interface CalculationResponse {
  id: string;
  expression: string;
  result: string;
  timestamp: string;
}

interface Calculation {
  id: string;
  expression: string;
  result: string;
  timestamp: Date;
}

export const mapCalculationResponse = (
  response: CalculationResponse
): Calculation => ({
  id: response.id,
  expression: response.expression,
  result: response.result,
  timestamp: new Date(response.timestamp),
});

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant C as Calculator Component
    participant A as API Client
    participant S as Server

    U->>C: Perform calculation
    C->>A: Send calculation request
    A->>S: POST /calculations
    S-->>A: Response with calculation data
    A-->>C: Return parsed calculation data
    C->>U: Display result and update history

For a calculator without external API integration, the data flow is simplified to local state management with no external dependencies. The API layer here serves as a foundation for potential future expansion where calculation history or cloud sync might be added.

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for calculator UI elements
  • Interaction to Next Paint (INP): <200ms for button interactions
  • Cumulative Layout Shift (CLS): <0.1 for all interactive elements

Bundle Size Budget per Route

  • Calculator feature bundle: <50KB (minified + gzipped)
  • No additional dependencies beyond standard React ecosystem

Critical Rendering Path Optimization

  • Critical CSS for calculator UI injected via inline styles or preload
  • All calculator components rendered synchronously without lazy loading
  • No layout thrashing from dynamic class manipulation during input

Image Optimization Strategy

  • No images required for calculator functionality
  • All UI elements rendered via CSS and SVG (no external image assets)
  • Font icons for operator symbols if needed

Font Loading Strategy

  • Use system fonts with fallbacks to ensure fast rendering
  • Critical font subset for calculator display (monospace) preloaded via <link rel="preload">
  • Font fallbacks for accessibility and performance

Memoization Strategy

// Memoize expensive calculations but not component renders
const CalculatorButton = React.memo(({ label, onClick }: CalculatorButtonProps) => {
  return (
    <button 
      onClick={onClick}
      className="calculator-button"
      aria-label={`Button: ${label}`}
    >
      {label}
    </button>
  );
});

const Display = React.memo(({ value }: DisplayProps) => {
  return (
    <div className="calculator-display" aria-live="polite">
      {value}
    </div>
  );
});

Virtualization for Large Lists

  • Not applicable - calculator has fixed set of 16 buttons
  • No need for virtualization as all elements render immediately

Prefetching and Preloading Strategy

  • No API calls or external resources to prefetch
  • All calculator logic runs client-side with no network dependency
  • Preload critical CSS and JavaScript bundles for fast initial render

Performance Monitoring

  • Monitor INP and CLS via Web Vitals API for real user feedback
  • Track button interaction latency using performance.mark/performance.measure
  • Implement synthetic monitoring for calculator responsiveness

Memory Management

  • No memory leaks expected in calculator state management
  • All event handlers properly cleaned up on component unmount
  • No long-lived subscriptions or intervals in calculator logic

Rendering Optimization

  • Use React.memo for static components like display and operator buttons
  • Avoid unnecessary re-renders through proper state management
  • Use useCallback for event handlers to prevent function recreation on every render

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries will be strategically placed to prevent cascading failures and provide graceful degradation:

graph TD
    A[App Root] --> B[CalculatorContainer]
    B --> C[DisplayComponent]
    B --> D[ButtonGrid]
    B --> E[HistoryPanel]
    F[ErrorBoundary] --> G[ErrorFallback]
  • CalculatorContainer as the primary error boundary to catch component-level errors
  • DisplayComponent and ButtonGrid will have their own error boundaries for isolated failures
  • HistoryPanel will be wrapped in a separate boundary to prevent history errors from breaking the calculator

Global Error Handler Design

// src/core/errorHandler.ts
import { captureException } from '@sentry/react';

interface ErrorHandlerConfig {
  logErrors: boolean;
  showToasts: boolean;
  fallbackComponent?: React.ComponentType<{ error: Error }>;
}

class GlobalErrorHandler {
  private config: ErrorHandlerConfig;

  constructor(config: ErrorHandlerConfig = { logErrors: true, showToasts: true }) {
    this.config = config;
  }

  handleError(error: Error, errorInfo: React.ErrorInfo): void {
    if (this.config.logErrors) {
      captureException(error, { contexts: { react: errorInfo } });
    }

    if (this.config.showToasts) {
      // Toast notification system would be implemented here
      this.showToast(error.message);
    }
  }

  private showToast(message: string): void {
    // Implementation for toast notifications
  }
}

export const globalErrorHandler = new GlobalErrorHandler();

Offline / Network Failure Handling

// src/core/offlineManager.ts
import { useEffect, useState } from 'react';

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

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

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

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

  return isOffline;
}

// Calculator component integration
const Calculator = () => {
  const isOffline = useOfflineStatus();
  
  if (isOffline) {
    return (
      <div className="offline-message">
        <p>Calculator is offline. Operations will be cached and synced when online.</p>
      </div>
    );
  }
  
  // Normal calculator rendering
};

Graceful Degradation Patterns

// src/components/Calculator/Calculator.tsx
const Calculator = () => {
  const [error, setError] = useState<string | null>(null);
  const [isOffline, setIsOffline] = useState(false);

  // Handle calculation errors
  const handleCalculationError = (errorMessage: string) => {
    setError(errorMessage);
    
    // Clear error after 3 seconds
    setTimeout(() => setError(null), 3000);
  };

  // Fallback UI for critical errors
  const renderErrorFallback = () => (
    <div className="error-fallback">
      <h3>Something went wrong</h3>
      <p>We're working to fix the issue. Please try again later.</p>
      <button onClick={() => window.location.reload()}>
        Refresh Page
      </button>
    </div>
  );

  // Offline fallback UI
  const renderOfflineFallback = () => (
    <div className="offline-fallback">
      <p>Calculator is offline</p>
      <p>Operations will be processed when connection is restored</p>
    </div>
  );

  if (isOffline) {
    return renderOfflineFallback();
  }

  if (error) {
    return renderErrorFallback();
  }

  // Normal calculator rendering
  return (
    <div className="calculator">
      {/* Calculator UI */}
    </div>
  );
};

User Feedback on Errors

// src/components/Toast/ToastManager.tsx
import React, { useState, useEffect } from 'react';

interface Toast {
  id: string;
  message: string;
  type: 'error' | 'warning' | 'success';
  duration?: number;
}

const ToastManager = () => {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: 'error' | 'warning' | 'success', duration = 5000) => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type, duration }]);
    
    // Auto-remove toast
    setTimeout(() => {
      setToasts(prev => prev.filter(t => t.id !== id));
    }, duration);
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(t => t.id !== id));
  };

  return (
    <div className="toast-container">
      {toasts.map(toast => (
        <Toast key={toast.id} toast={toast} onRemove={removeToast} />
      ))}
    </div>
  );
};

const Toast = ({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) => {
  return (
    <div className={`toast toast--${toast.type}`}>
      <span>{toast.message}</span>
      <button onClick={() => onRemove(toast.id)}>×</button>
    </div>
  );
};

Error Recovery Strategy

// src/core/calculationEngine.ts
class CalculationEngine {
  private errorHistory: Error[] = [];

  async executeCalculation(input: string): Promise<number | null> {
    try {
      // Validate input
      this.validateInput(input);
      
      // Perform calculation
      const result = this.performCalculation(input);
      
      // Clear error history on successful operation
      this.errorHistory = [];
      
      return result;
    } catch (error) {
      if (error instanceof Error) {
        this.errorHistory.push(error);
        
        // Handle specific error types
        if (this.isRecoverableError(error)) {
          return this.handleRecoverableError(error);
        }
        
        // Log and rethrow for global handling
        throw error;
      }
      
      return null;
    }
  }

  private isRecoverableError(error: Error): boolean {
    const recoverableErrors = [
      'Division by zero',
      'Invalid syntax',
      'Overflow error'
    ];
    
    return recoverableErrors.some(msg => error.message.includes(msg));
  }

  private handleRecoverableError(error: Error): number | null {
    // Log the error for monitoring
    console.warn('Recoverable calculation error:', error.message);
    
    // Return default value or last known good state
    return 0;
  }

  private validateInput(input: string): void {
    if (!input || input.trim() === '') {
      throw new Error('Empty input provided');
    }
    
    // Additional validation logic
  }

  private performCalculation(input: string): number {
    // Safe evaluation with proper error handling
    try {
      return eval(input); // In production, use safer alternatives like math.js
    } catch (error) {
      throw new Error(`Invalid calculation: ${input}`);
    }
  }
}

Error Boundary Implementation

// src/components/ErrorBoundary/ErrorBoundary.tsx
import React from 'react';

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback?: React.ComponentType<{ error: Error }> },
  ErrorBoundaryState
> {
  constructor(props: { children: React.ReactNode; fallback?: React.ComponentType<{ error: Error }> }) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    console.error('Calculator component error:', error, errorInfo);
  }

  render(): React.ReactNode {
    if (this.state.hasState) {
      const FallbackComponent = this.props.fallback || DefaultFallback;
      return <FallbackComponent error={this.state.error!} />;
    }

    return this.props.children;
  }
}

const DefaultFallback = ({ error }: { error: Error }) => (
  <div className="error-boundary-fallback">
    <h3>Something went wrong</h3>
    <p>{error.message}</p>
    <button onClick={() => window.location.reload()}>
      Reload Page
    </button>
  </div>
);

export default ErrorBoundary;

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target):

  • Core Calculator component with all button interactions
  • Display component with value formatting and overflow handling
  • History panel component with scroll and clear functionality
  • Input validation and error state components

Utility Tests (90% coverage target):

  • Expression parser and evaluator functions
  • Format number utility with locale-aware formatting
  • History storage and retrieval utilities
  • Keyboard event handler mapping

Test Coverage Targets:

// Example test coverage structure
{
  "Calculator": {
    "render": 100,
    "buttonClicks": 95,
    "errorHandling": 90
  },
  "Utilities": {
    "expressionEvaluator": 100,
    "formatNumber": 95,
    "historyManager": 90
  }
}

Integration Testing Strategy

Core User Flows:

  • Basic arithmetic operations (addition, subtraction, multiplication, division)
  • Complex expression evaluation with parentheses
  • History navigation and clearing
  • Keyboard input integration
  • Error state transitions (division by zero, overflow)

Cross-component Tests:

  • Calculator display updates with button presses
  • History panel sync with calculation history
  • Keyboard shortcuts integration with UI state
  • Responsive behavior across device sizes

E2E Testing Scope

Tools: Playwright (for cross-browser compatibility and headless testing)

Critical User Journeys:

  1. Basic calculation flow (1+2=3)
  2. Complex expression evaluation (2*(3+4)=14)
  3. History navigation and clearing
  4. Keyboard-only operation (tab navigation, enter submission)
  5. Error state recovery (division by zero -> clear -> retry)

Test Coverage:

// E2E test structure example
describe('Calculator E2E', () => {
  test('should perform basic arithmetic operations', async ({ page }) => {
    // Test 1+2=3 flow
  });
  
  test('should handle complex expressions', async ({ page }) => {
    // Test (2+3)*4=20
  });
  
  test('should manage history operations', async ({ page }) => {
    // Test calculation -> view history -> clear history
  });
});

Visual Regression Testing

Tool: Percy (or similar visual testing platform)

Test Scenarios:

  • Calculator in default, dark, and light themes
  • Different screen sizes (mobile, tablet, desktop)
  • Error states with visual indicators
  • History panel expansion/collapse states
  • Hover/focus states for all buttons

Accessibility Testing Automation

Tool: axe-core with Cypress integration

Testing Coverage:

  • Keyboard navigation completeness (tab order, focus management)
  • ARIA attribute correctness for all interactive elements
  • Color contrast compliance (4.5:1 minimum)
  • Screen reader compatibility for all states
  • Focus visibility and trap management

Test Data Strategy

Test Fixtures:

// Sample test data structure
const testCases = {
  basicOperations: [
    { input: '2+2', expected: '4' },
    { input: '10-5', expected: '5' },
    { input: '3*4', expected: '12' },
    { input: '8/2', expected: '4' }
  ],
  edgeCases: [
    { input: '0/5', expected: '0' },
    { input: '1/0', error: 'Division by zero' },
    { input: '999999999999', overflow: true }
  ],
  complexExpressions: [
    { input: '(2+3)*4', expected: '20' },
    { input: '2*(3+4)', expected: '14' }
  ]
};

Test Data Generation:

  • Parameterized test cases for all supported operations
  • Randomized input generation for edge case testing
  • Internationalized number formatting test cases
  • Multi-language character set support in tests

Test Environment Configuration

Isolation Strategy:

  • Mocked localStorage for history persistence
  • Stubbed window.navigator for keyboard event testing
  • Isolated DOM environment for component rendering tests
  • Test database with clean state between runs

Continuous Integration:

  • Unit tests run on every commit (fast feedback)
  • Integration tests on PR merge validation
  • E2E tests on feature branch deployments (headless)
  • Visual regression tests on design system changes

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The calculator must meet WCAG 2.1 AA standards with the following key requirements:

  • Keyboard Navigation: All calculator functions must be operable via keyboard (Tab, Enter, Space)
  • Focus Management: Clear visual focus indicators and proper focus order
  • Screen Reader Compatibility: All buttons and display must be properly labeled for screen readers
  • Contrast Ratios: Minimum 4.5:1 contrast ratio for text and interactive elements
  • Input Validation: Clear error states with accessible feedback when invalid operations are attempted

Keyboard navigation support

All calculator functionality must be fully operable using only keyboard input:

  • Tab Navigation: Buttons should receive focus in logical order (numbers, operators, equals, clear)
  • Enter/Space Activation: All interactive elements should respond to Enter or Space key presses
  • Escape Key: Clear button should be accessible via Escape key for quick reset
  • Arrow Navigation: For complex operations, arrow keys should navigate between display and buttons

Focus management strategy

Focus management is critical for keyboard-only users:

stateDiagram-v2
    [*] --> Idle
    Idle --> ActiveButton: Tab to button
    ActiveButton --> Display: Button pressed
    Display --> Idle: Operation complete
    ActiveButton --> ActiveButton: Keyboard repeat
  • Focus is programmatically managed when operations complete (e.g., after pressing equals)
  • Focus returns to the display element to indicate current state
  • Focus trapping is not needed as calculator has a single logical interaction flow
  • Focus restoration after modal interactions (if any) is handled via proper state management

ARIA roles and attributes plan

ARIA attributes ensure screen reader accessibility:

// CalculatorButton.tsx
interface CalculatorButtonProps {
  label: string;
  ariaLabel?: string;
  onClick: () => void;
  type: 'digit' | 'operator' | 'function';
  disabled?: boolean;
}

const CalculatorButton: React.FC<CalculatorButtonProps> = ({
  label,
  ariaLabel,
  onClick,
  type,
  disabled = false
}) => {
  return (
    <button
      aria-label={ariaLabel || label}
      role={type === 'digit' ? 'button' : 'button'}
      onClick={onClick}
      disabled={disabled}
      className="calculator-button"
    >
      {label}
    </button>
  );
};
  • aria-label provides screen reader context for symbols (e.g., "plus" for "+")
  • role="button" ensures proper semantic interpretation
  • disabled state is properly communicated via ARIA attributes
  • aria-live="polite" on display element for screen reader updates

Screen reader compatibility

Screen readers must receive clear, contextual information:

  • Display Updates: When calculation results change, announce the new value using aria-live="polite"
  • Error States: When invalid operations occur, announce error messages with aria-live="assertive"
  • Button Labels: All buttons have descriptive labels that screen readers can read
  • Operation Announcements: Complex operations (e.g., "12 + 5 = 17") are announced clearly
// CalculatorDisplay.tsx
interface CalculatorDisplayProps {
  value: string;
  error?: string;
}

const CalculatorDisplay: React.FC<CalculatorDisplayProps> = ({ value, error }) => {
  return (
    <div 
      className="calculator-display"
      aria-live={error ? "assertive" : "polite"}
      aria-atomic="true"
    >
      {error ? (
        <span className="error-message" aria-label="Error: Invalid operation">
          {error}
        </span>
      ) : (
        <span aria-label={`Current result: ${value}`}>{value}</span>
      )}
    </div>
  );
};

Focus indicators and visual design

  • High Contrast: Buttons must have sufficient contrast (4.5:1) with background
  • Visual Focus: Focus rings must be clearly visible and not visually obscured by other elements
  • Consistent Focus Order: Tab order should match logical operation flow (digits → operators → functions)
  • Hover and Focus States: Both states should have clear visual distinction

Alternative input methods

The calculator should support:

  • Voice Input: Integration with speech recognition APIs where available
  • Touch Support: Large touch targets (minimum 44px) for mobile users
  • High Contrast Mode: Support for OS-level high contrast themes
  • Zoom Compatibility: Text and elements should scale appropriately with browser zoom

Testing strategy for accessibility

  • Automated Testing: Use axe-core or similar tools to scan for WCAG violations
  • Manual Keyboard Testing: Verify all functionality works via keyboard navigation alone
  • Screen Reader Testing: Test with NVDA, JAWS, and VoiceOver for proper announcement
  • Color Contrast Testing: Validate all color combinations meet AA standards
  • Focus Order Verification: Confirm logical tab order and focus management

Keyboard accessibility considerations

All calculator operations must be operable via keyboard:

  1. Digit Entry: All numeric keys (0-9) must be accessible
  2. Operator Navigation: +, -, ×, ÷ keys must respond to keyboard input
  3. Function Keys: Enter (equals), Escape (clear), Backspace (delete) must work
  4. Decimal Point: Period key should be accessible for decimal input
  5. Parentheses: Open and close parenthesis keys must be keyboard-accessible

Dynamic content updates

The calculator display must properly announce content changes:

// CalculatorDisplay.tsx (continued)
const CalculatorDisplay: React.FC<CalculatorDisplayProps> = ({ value, error }) => {
  const [lastValue, setLastValue] = useState<string>('');
  
  useEffect(() => {
    if (value !== lastValue) {
      // Announce value changes for screen readers
      const displayElement = document.querySelector('.calculator-display');
      if (displayElement) {
        displayElement.setAttribute('aria-label', `Result: ${value}`);
      }
      setLastValue(value);
    }
  }, [value, lastValue]);

  return (
    <div 
      className="calculator-display"
      aria-live={error ? "assertive" : "polite"}
      aria-atomic="true"
    >
      {error ? (
        <span className="error-message" aria-label="Error: Invalid operation">
          {error}
        </span>
      ) : (
        <span aria-label={`Current result: ${value}`}>{value}</span>
      )}
    </div>
  );
};

10. Security Considerations

XSS Prevention Strategy

The calculator must prevent cross-site scripting attacks through comprehensive input sanitization and output encoding:

// Input sanitization for calculator expressions
const sanitizeInput = (input: string): string => {
  // Remove potentially dangerous characters except allowed ones
  const allowedChars = /^[0-9+\-*/().% ]+$/;
  if (!allowedChars.test(input)) {
    throw new Error('Invalid character in input');
  }
  
  // Prevent JavaScript injection
  if (input.includes('javascript:') || input.includes('script')) {
    throw new Error('Potentially malicious input detected');
  }
  
  return input.trim();
};

// Safe expression evaluation with sandboxed execution
const evaluateExpression = (expression: string): number => {
  // Validate expression structure before evaluation
  const safeExpression = sanitizeInput(expression);
  
  // Use a safe evaluation approach (avoid eval)
  const result = math.evaluate(safeExpression);
  
  return result;
};

CSRF Protection

For calculator features that involve user data persistence or state modifications, implement CSRF token validation:

// CSRF token management in calculator context
const useCSRFToken = () => {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);
  
  useEffect(() => {
    // Fetch CSRF token from server on component mount
    const fetchCSRFToken = async () => {
      try {
        const response = await fetch('/api/csrf-token', {
          method: 'GET',
          credentials: 'same-origin'
        });
        
        const { token } = await response.json();
        setCsrfToken(token);
      } catch (error) {
        console.error('Failed to fetch CSRF token:', error);
      }
    };
    
    fetchCSRFToken();
  }, []);
  
  return csrfToken;
};

Content Security Policy (CSP) Headers

Configure CSP headers to prevent unauthorized script execution:

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

Sensitive Data Handling in Frontend State

Calculator state should not store sensitive user data or authentication tokens:

// Secure calculator state management
interface CalculatorState {
  // Only store calculation history, not sensitive data
  history: string[];
  currentInput: string;
  result: number | null;
  // Avoid storing user credentials or session tokens
}

// Secure state update function
const updateCalculatorState = (
  prevState: CalculatorState,
  newState: Partial<CalculatorState>
): CalculatorState => {
  // Validate that no sensitive data is being stored
  const { history, currentInput, result } = newState;
  
  if (history && history.some(item => item.includes('token') || item.includes('auth'))) {
    throw new Error('Sensitive data detected in calculator history');
  }
  
  return {
    ...prevState,
    ...newState
  };
};

Input Sanitization Approach

Implement comprehensive input validation to prevent injection attacks:

// Comprehensive calculator input sanitization
class CalculatorInputValidator {
  private static readonly ALLOWED_OPERATORS = ['+', '-', '*', '/', '(', ')', '.', '%'];
  private static readonly MAX_EXPRESSION_LENGTH = 100;
  
  static validateExpression(expression: string): boolean {
    // Length check
    if (expression.length > this.MAX_EXPRESSION_LENGTH) {
      return false;
    }
    
    // Character validation
    const validChars = new RegExp(`^[0-9${this.ALLOWED_OPERATORS.join('')} ]+$`);
    if (!validChars.test(expression)) {
      return false;
    }
    
    // Balanced parentheses check
    if (!this.isParenthesesBalanced(expression)) {
      return false;
    }
    
    // Prevent consecutive operators
    if (this.hasConsecutiveOperators(expression)) {
      return false;
    }
    
    // Prevent invalid operator sequences
    if (this.hasInvalidOperatorSequences(expression)) {
      return false;
    }
    
    return true;
  }
  
  private static isParenthesesBalanced(expression: string): boolean {
    let count = 0;
    for (const char of expression) {
      if (char === '(') count++;
      if (char === ')') count--;
      if (count < 0) return false;
    }
    return count === 0;
  }
  
  private static hasConsecutiveOperators(expression: string): boolean {
    const consecutiveOps = /[\+\-\*\/]{2,}/;
    return consecutiveOps.test(expression);
  }
  
  private static hasInvalidOperatorSequences(expression: string): boolean {
    // Prevent operators at start or end
    if (/^[\+\-\*\/]|[\+\-\*\/]$/g.test(expression)) {
      return true;
    }
    
    // Prevent operators before/after parentheses
    if (/\(\s*[\+\-\*\/]/g.test(expression) || /[\+\-\*\/]\s*\)/g.test(expression)) {
      return true;
    }
    
    return false;
  }
}

Security Implementation Summary

  1. Input Validation: All user input is strictly validated to prevent injection attacks
  2. Expression Safety: Uses math.js or similar safe evaluation libraries instead of eval()
  3. CSRF Protection: Token-based validation for persistent calculator features
  4. CSP Compliance: Content Security Policy headers prevent unauthorized script execution
  5. Data Isolation: Calculator state does not store sensitive user information
  6. Rate Limiting: API endpoints should implement rate limiting to prevent abuse

The calculator implementation follows security best practices by:

  • Validating all user inputs before processing
  • Using safe mathematical evaluation libraries
  • Implementing proper CSRF protection for stateful features
  • Maintaining strict separation between calculator data and sensitive user information
  • Following CSP guidelines to prevent XSS vulnerabilities

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Translation keys will follow a hierarchical structure using dot notation for clear organization:

calculator:
  button:
    clear: "Clear"
    equals: "Equals"
    decimal: "Decimal"
    operator:
      add: "Add"
      subtract: "Subtract"
      multiply: "Multiply"
      divide: "Divide"
  error:
    invalid-operation: "Invalid operation"
    division-by-zero: "Division by zero"
  tooltip:
    clear: "Clear calculator"
    equals: "Calculate result"

RTL Support Considerations

RTL support will be implemented using CSS logical properties and direction-aware layout:

  • All UI elements will respect direction: rtl in the root document
  • Button grid layout will reverse order in RTL mode
  • Number formatting will adjust alignment and grouping separators
  • Input field styling will adapt to RTL text direction

Date, Number, Currency Formatting Strategy

Number formatting will leverage the browser's Intl API for locale-aware formatting:

  • Decimal separator: localized (e.g., comma in German, period in English)
  • Grouping separator: localized (e.g., space in French, comma in German)
  • Negative number formatting: localized with proper minus sign
  • Display precision: configurable based on locale (e.g., 2 decimal places in most locales)

Locale Detection and Switching Strategy

Automatic Detection:

  • Primary detection from navigator.language or navigator.languages
  • Fallback to English (en-US) if no supported locale detected
  • User preference stored in localStorage for persistence across sessions

Manual Switching:

  • Language switcher component with flag icons
  • Available locales: en-US, fr-FR, de-DE, es-ES
  • Language preference persisted in localStorage and session storage
  • Real-time UI update without page refresh using React Context
// i18n types and context
interface Translation {
  calculator: {
    button: {
      clear: string;
      equals: string;
      decimal: string;
      operator: {
        add: string;
        subtract: string;
        multiply: string;
        divide: string;
      };
    };
    error: {
      invalid-operation: string;
      division-by-zero: string;
    };
    tooltip: {
      clear: string;
      equals: string;
    };
  };
}

type Locale = 'en-US' | 'fr-FR' | 'de-DE' | 'es-ES';

interface I18nContextType {
  locale: Locale;
  translations: Translation;
  changeLocale: (newLocale: Locale) => void;
  formatNumber: (value: number) => string;
  formatDate: (date: Date) => string;
}

const I18nContext = React.createContext<I18nContextType>({
  locale: 'en-US',
  translations: {} as Translation,
  changeLocale: () => {},
  formatNumber: (value) => value.toString(),
  formatDate: (date) => date.toISOString(),
});

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The calculator feature should track key performance indicators to understand user behavior and system health:

  • First Input Delay (FID): Target < 100ms for interactive response times
  • Largest Contentful Paint (LCP): Target < 2.5s for content visibility
  • Cumulative Layout Shift (CLS): Target < 0.1 for visual stability
  • Time to Interactive (TTI): Target < 5s for full interactivity
  • Calculator Usage Frequency: Track how often users perform calculations
  • Error Rate: Monitor calculation errors and input validation failures

Error Tracking Integration

Integrate with error monitoring platforms like Sentry or Datadog:

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

// Global error boundary for unhandled errors
class CalculatorErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    Sentry.captureException(error, {
      contexts: { calculator: { errorInfo } }
    });
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong with the calculator.</div>;
    }
    return this.props.children;
  }
}

Custom Events and User Journey Tracking

Implement custom event tracking for key calculator interactions:

// Event tracking utility
const trackCalculatorEvent = (eventName: string, payload?: any) => {
  // Send to analytics service
  if (window.gtag) {
    window.gtag('event', eventName, {
      ...payload,
      timestamp: Date.now(),
      userId: getCurrentUserId()
    });
  }
};

// Example usage in calculator component
const handleButtonClick = (buttonValue: string) => {
  trackCalculatorEvent('calculator_button_click', {
    button: buttonValue,
    calculationStep: getCurrentCalculationStep()
  });
};

Performance Monitoring and Regression Detection

Monitor performance regressions through automated dashboards:

  • Calculation Response Time: Track average time per operation
  • Memory Usage: Monitor for memory leaks in calculation state
  • Bundle Size Changes: Track against baseline for any increase
  • Network Requests: Monitor API calls if calculator integrates with external services

Implement performance monitoring hooks:

// Performance monitoring hook
const useCalculatorPerformance = () => {
  const [metrics, setMetrics] = useState({
    calculationTime: 0,
    memoryUsage: 0,
    errorCount: 0
  });

  const measureCalculation = (operation: string, callback: () => any) => {
    const start = performance.now();
    try {
      const result = callback();
      const end = performance.now();
      
      setMetrics(prev => ({
        ...prev,
        calculationTime: end - start
      }));
      
      return result;
    } catch (error) {
      setMetrics(prev => ({
        ...prev,
        errorCount: prev.errorCount + 1
      }));
      throw error;
    }
  };

  return { metrics, measureCalculation };
};

Dashboard and Alerting Strategy

Create dashboards for:

  • Real-time calculator usage statistics
  • Error rate trends over time
  • Performance degradation alerts
  • User session duration and engagement patterns

Set up automated alerts when:

  • Error rate exceeds 2% of total operations
  • Calculation response time increases by 30%
  • Memory usage grows beyond threshold

Data Privacy and Compliance Considerations

Ensure monitoring respects user privacy:

  • Anonymize user IDs in tracking
  • Exclude sensitive calculation data from logs
  • Comply with GDPR/CCPA for user data collection
  • Implement opt-out mechanisms for analytics tracking

Integration with Existing Monitoring Infrastructure

The calculator monitoring should integrate with existing observability tools:

  • Send metrics to Prometheus/Grafana for dashboarding
  • Log events to ELK stack for search and analysis
  • Integrate with existing error tracking systems
  • Support APM tools for end-to-end transaction tracing

User Behavior Analytics

Track user journey patterns:

  • Most frequently used operations
  • Common calculation sequences
  • Drop-off points in multi-step calculations
  • User session length and engagement patterns

This monitoring strategy provides comprehensive visibility into calculator performance, user experience, and system reliability while maintaining privacy compliance.

13. File & Folder Structure

Proposed Directory Structure

/src
  /components
    /calculator
      Calculator.tsx
      CalculatorButton.tsx
      Display.tsx
      index.ts
  /hooks
    useCalculator.ts
  /types
    calculator.types.ts
  /utils
    calculator.utils.ts
  /styles
    calculator.module.css

Module Boundaries and Import Rules

  • Calculator feature boundary: All calculator-related components, hooks, and utilities live within /components/calculator and /hooks/useCalculator.ts
  • No cross-feature imports: Calculator components cannot import directly from other features (e.g., dashboard, profile)
  • Barrel exports: index.ts files provide clean entry points for the calculator feature

Shared vs Feature-Specific Code Separation

  • Shared: Common UI patterns, utility functions, and types that could be reused across features (e.g., Button, Typography)
  • Feature-specific: Calculator logic, state management, and UI components that are only relevant to this feature

The structure follows a feature-based organization pattern, which aligns with the calculator being a self-contained feature with clear boundaries and dependencies. This approach reduces cognitive load for developers working on the calculator while maintaining clean separation from other features in the application.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React (v18+)

  • Justification: Core UI library with concurrent rendering, hooks, and ecosystem compatibility
  • Bundle impact: ~150KB (minified)
  • Alternatives considered: Preact - smaller bundle but less ecosystem support

2. TypeScript (v5+)

  • Justification: Type safety for robust codebase, better developer experience, and compile-time error detection
  • Bundle impact: None (compile-time only)
  • Alternatives considered: JavaScript - less safe and maintainable

3. React Testing Library (v14+)

  • Justification: Test DOM interactions as users do, better test maintainability and reliability
  • Bundle impact: ~50KB (test-only)
  • Alternatives considered: Enzyme - more complex API, less reliable for React 18

4. Tailwind CSS (v3+)

  • Justification: Utility-first CSS framework for rapid UI development, consistent design system
  • Bundle impact: ~10KB (minified)
  • Alternatives considered: Styled Components - more complex for simple UIs

5. React Hook Form (v7+)

  • Justification: High-performance form validation with minimal re-renders, built-in error handling
  • Bundle impact: ~15KB (minified)
  • Alternatives considered: Manual form handling - more boilerplate and error-prone

6. React Query (v5+)

  • Justification: Server state management for API data, caching, and automatic refetching
  • Bundle impact: ~20KB (minified)
  • Alternatives considered: Redux Toolkit - more complex for simple state needs

7. date-fns (v3+)

  • Justification: Modern, modular date formatting and manipulation with zero dependencies
  • Bundle impact: ~5KB (minified)
  • Alternatives considered: Moment.js - large bundle, mutating methods, deprecated

Alternatives Considered and Rejected

1. Redux Toolkit

  • Rejected: Overkill for calculator's simple state needs; React Query handles server state better
  • Reasoning: Calculator has minimal local state, no complex global state management required

2. Material UI

  • Rejected: Too heavy for calculator's simple UI requirements; Tailwind provides better customization
  • Reasoning: Calculator requires minimal styling, Material UI adds unnecessary complexity and bundle size

3. MobX

  • Rejected: More complex than needed for calculator state management
  • Reasoning: React Query + local state is simpler and more maintainable for this use case

4. Jest

  • Rejected: Testing library is more appropriate for component testing in React ecosystem
  • Reasoning: React Testing Library provides better integration with React's testing patterns

Licensing and Bundle Size Impact Assessment

License Compliance:

  • All libraries are MIT licensed or compatible
  • No proprietary dependencies that would require commercial licensing

Bundle Size Budget:

  • Total frontend bundle: ~250KB (excluding dev dependencies)
  • Calculator-specific libraries: ~100KB (30% of total bundle)
  • Critical path dependencies: React + TypeScript + Tailwind + Testing Library = ~180KB

Performance Impact:

  • Tree-shaking enabled for all libraries
  • Development vs production builds properly configured
  • Critical CSS extracted and inlined for faster rendering

Build Tooling:

  • Vite or Webpack 5+ with proper configuration for optimal bundling
  • Code splitting and lazy loading implemented where appropriate
  • Bundle analyzer integration for ongoing monitoring

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Calculator mode support: Should we support scientific calculator features (trigonometric functions, logarithms) or stick to basic arithmetic? This decision impacts component complexity and state management requirements.
  • Keyboard support scope: Should we implement full keyboard accessibility or just basic numeric input? This affects the complexity of our key event handling and state machine.
  • History storage strategy: Should calculation history persist across sessions or be cleared on app restart? This impacts state management approach and storage strategy.

Known trade-offs made in this design

  • Simplified UI state machine: The calculator uses a minimal state machine that doesn't handle edge cases like consecutive operators or decimal point placement errors. This trade-off prioritizes simplicity over robustness.
  • No complex expression parsing: The calculator evaluates expressions sequentially rather than supporting full mathematical expression parsing, which limits advanced use cases but simplifies implementation.
  • Single calculation context: All calculations are performed in a single context, rather than supporting multiple simultaneous calculations or memory operations.

Risks and mitigations

  • Input validation risk: Risk of invalid input causing calculation errors or UI crashes. Mitigation: Comprehensive input sanitization and error boundary implementation.
  • State inconsistency risk: Risk of calculator state becoming inconsistent due to complex operator precedence handling. Mitigation: Immutable state updates and comprehensive test coverage.
  • Performance degradation risk: Risk of performance issues with very large numbers or complex calculations. Mitigation: Number formatting and calculation limits, with graceful degradation for edge cases.

Technical debt acknowledged

  • Limited operator support: The current design only supports basic arithmetic operators (+, -, *, /) and doesn't include advanced operations like exponentiation or modulo. This creates technical debt for future feature additions.
  • No memory register functionality: The calculator lacks support for storing and recalling values (M+, M-, MR, MC), which is a common feature in full-featured calculators and would require significant refactoring to add later.
  • Hardcoded button layout: The calculator's button arrangement is hardcoded rather than dynamically configurable, which limits flexibility for future UI adaptations or different calculator types.
  • No expression history: The calculator doesn't provide a way to view or edit previous calculations, which is a common feature in advanced calculators and would require additional state management complexity.

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.