Skip to main content

Like/Reaction System Design Patterns and Implementation

Comprehensive low-level design for scalable like/reaction systems with real-time updates, caching strategies, and conflict resolution mechanisms.

This document presents a production-ready low-level design for implementing scalable like and reaction systems, covering data models, API considerations, real-time synchronization, and performance optimization strategies for modern web applications.

1. Problem Statement & Requirements

Clarifying questions

  1. What types of reactions are supported (like, love, haha, wow, sad, angry) or should this be extensible?
  2. Should reactions be tied to specific content types (posts, comments, media) or generic across all content?
  3. Do we need to support real-time updates for reactions (e.g., live counters, instant reaction display)?
  4. Should reactions persist offline and sync when connectivity is restored?
  5. What's the expected scale of reactions (e.g., millions of reactions per content item)?
  6. Are there any accessibility requirements beyond basic keyboard navigation and screen reader support?
  7. Do we need to support internationalization for reaction labels or emojis?

Functional requirements

  • Users can like or react to content items (posts, comments, media)
  • Reaction counts update in real-time for all users
  • Users can remove their reactions
  • Users can see their own reaction status (highlighted or marked)
  • Reaction types should support at least 5 different emojis/labels
  • Users can view all reactions on a content item with user avatars and timestamps
  • Reaction history should be accessible for moderation or analytics

Non-functional requirements

  • Performance: Reaction counters must update within 200ms of user action
  • Accessibility: Full keyboard navigation and screen reader support with proper ARIA labels
  • Scalability: System must handle millions of reactions per content item without performance degradation
  • Reliability: Reaction operations should be idempotent and resilient to network failures
  • Consistency: Reaction counts must be eventually consistent across all clients

Constraints and assumptions

  • Target browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
  • No backend authentication required for this feature (assumes auth handled at API layer)
  • Reaction counts are displayed as integers with comma separators for large numbers
  • Users can only have one reaction per content item (no multiple reactions)
  • Reaction data is stored in a normalized database structure
  • Content items are identified by unique IDs (UUID or numeric IDs)
  • No rate limiting on reactions at the frontend level

Out of scope

  • Reaction analytics or trending data
  • Reaction notifications to content creators
  • Admin moderation tools for reactions
  • Emoji customization or user-defined reaction types
  • Reaction history audit trails
  • Cross-platform reaction synchronization (e.g., mobile vs. web)
  • Multi-language reaction labels beyond basic emoji support
  • Reaction-based content ranking or filtering

2. Component Architecture

graph TD
    A[ReactionSystem] --> B[ReactionButton]
    A --> C[ReactionCount]
    A --> D[ReactionList]
    B --> E[ReactionModal]
    D --> F[ReactionItem]
    E --> G[ReactionUserList]
    
    classDef smart fill:#4CAF50,stroke:#333,color:white;
    classDef dumb fill:#2196F3,stroke:#333,color:white;
    
    class A,B,C,D,E,F,G smart
    class A,B,C,D,E,F,G dumb

Component Tree and Breakdown

The ReactionSystem is a composite component that orchestrates the entire like/reaction functionality. It contains:

  • Smart Components (Container):

    • ReactionSystem - Main container managing state and API interactions
    • ReactionButton - Interactive button that toggles reactions
    • ReactionList - Displays all reactions for an item
  • Dumb Components (Presentational):

    • ReactionCount - Displays reaction counts
    • ReactionItem - Individual reaction display with avatar and timestamp
    • ReactionModal - Modal showing all users who reacted
    • ReactionUserList - List of users who reacted to an item

TypeScript Props Contracts

// ReactionSystem Props
interface ReactionSystemProps {
  itemId: string;
  initialReactions?: Reaction[];
  maxVisibleReactions?: number;
  currentUser?: User;
  onReactionChange?: (reaction: Reaction) => void;
}

// ReactionButton Props
interface ReactionButtonProps {
  itemId: string;
  reactions: Reaction[];
  currentUser?: User;
  onReactionToggle: (reactionType: string) => void;
  disabled?: boolean;
}

// ReactionList Props
interface ReactionListProps {
  reactions: Reaction[];
  currentUser?: User;
  maxVisible?: number;
  onReactionClick: (reactionType: string) => void;
}

// ReactionItem Props
interface ReactionItemProps {
  reaction: Reaction;
  currentUser?: User;
  onClick?: () => void;
}

// ReactionModal Props
interface ReactionModalProps {
  reactions: Reaction[];
  onClose: () => void;
  currentUser?: User;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> ShowingModal : open_modal
    ShowingModal --> Idle : close_modal
    Idle --> TogglingReaction : toggle_reaction
    TogglingReaction --> Idle : reaction_success
    TogglingReaction --> Error : reaction_error
    
    state TogglingReaction {
        [*] --> SendingRequest
        SendingRequest --> Processing : response_received
        Processing --> Idle : success
        Processing --> Error : failure
    }
    
    state Error {
        [*] --> DisplayingError
        DisplayingError --> Idle : retry
    }

Component Responsibilities

  • ReactionSystem:

    • Manages overall state and API interactions
    • Handles reaction creation/deletion
    • Coordinates between child components
  • ReactionButton:

    • Renders the primary interaction element
    • Handles click events to toggle reactions
    • Shows current user's reaction state
  • ReactionList:

    • Displays all reactions in a compact view
    • Shows reaction counts per type
    • Provides access to full reaction modal
  • ReactionItem:

    • Renders individual reaction with avatar and timestamp
    • Handles user interaction for specific reactions
  • ReactionModal:

    • Shows all users who reacted to an item
    • Provides detailed view of reactions
    • Handles user navigation within modal

Reusability Decisions

The component architecture is designed for maximum reusability:

  1. Modular Design: Each component has a single responsibility and can be reused in different contexts
  2. Configurable Props: Components accept configuration through props to adapt to various use cases
  3. Type Safety: Strong TypeScript contracts ensure safe reuse across different parts of the application
  4. Themeable: Components can be styled to match various design systems

Component Lifecycle Considerations

  • Mounting: Fetch initial reactions for the item
  • Updating: Handle reaction changes from other users or external sources
  • Unmounting: Clean up any ongoing requests or subscriptions
  • Error Handling: Gracefully handle network failures during reactions
  • Performance: Implement proper memoization to prevent unnecessary re-renders

The component lifecycle is optimized for real-time updates with proper cleanup and error handling to ensure a smooth user experience.

3. State Management Design

Local state vs. global state decisions

For a like/reaction system, we need to balance between local component state for immediate UI feedback and global state for consistency across the application. The core decision is:

  • Local state: Immediate UI feedback (like animation, hover states, reaction count updates)
  • Global state: Persistent user reactions, server synchronization, and cross-component consistency

State management approach

Zustand is selected as the primary state management solution because:

  1. Simplicity: Zero dependencies, minimal boilerplate
  2. Flexibility: Easy to integrate with React Query for server state
  3. TypeScript support: Excellent type inference and safety
  4. Performance: Optimized for React's rendering model

State shape and normalization strategy

// Normalized state structure for efficient updates
interface Reaction {
  id: string;
  userId: string;
  reactionType: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry';
  timestamp: number;
}

interface ReactionCounts {
  like: number;
  love: number;
  haha: number;
  wow: number;
  sad: number;
  angry: number;
  total: number;
}

interface ReactionState {
  reactionsByEntity: Record<string, Reaction[]>;
  userReactions: Record<string, string[]>; // entityID -> reactionType[]
  reactionCounts: Record<string, ReactionCounts>;
  isFetching: Record<string, boolean>;
  error: Record<string, string | null>;
}

Server state vs. client state separation

Client State:

  • Immediate UI feedback (optimistic updates)
  • User's current reactions
  • Local validation states

Server State:

  • Persistent reaction history
  • Reaction counts from all users
  • User permissions and validation

Cache invalidation strategy

  1. Entity-level cache invalidation: When reactions change for an entity, invalidate that specific entity's cache
  2. Timestamp-based invalidation: Use server timestamps to determine freshness
  3. Manual refresh triggers: Allow users to manually refresh reactions

Optimistic updates design

Optimistic updates are critical for user experience:

  1. Immediate UI response: Update counts and user reactions instantly
  2. Rollback on failure: Revert changes if server request fails
  3. Conflict resolution: Handle concurrent updates gracefully

Critical-path hook implementation

// reactions.hook.ts
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { Reaction, ReactionCounts } from './types';

interface ReactionsStore {
  reactionsByEntity: Record<string, Reaction[]>;
  userReactions: Record<string, string[]>;
  reactionCounts: Record<string, ReactionCounts>;
  isFetching: Record<string, boolean>;
  error: Record<string, string | null>;
  
  // Actions
  addReaction: (entityId: string, reactionType: string) => Promise<void>;
  removeReaction: (entityId: string, reactionType: string) => Promise<void>;
  fetchReactions: (entityId: string) => Promise<void>;
  clearEntity: (entityId: string) => void;
}

const useReactionsStore = create<ReactionsStore>()(
  immer((set, get) => ({
    reactionsByEntity: {},
    userReactions: {},
    reactionCounts: {},
    isFetching: {},
    error: {},
    
    addReaction: async (entityId, reactionType) => {
      const existingUserReactions = get().userReactions[entityId] || [];
      
      // Optimistic update - immediate UI feedback
      set((state) => {
        const currentCounts = state.reactionCounts[entityId] || {
          like: 0, love: 0, haha: 0, wow: 0, sad: 0, angry: 0, total: 0
        };
        
        // Update counts optimistically
        const newCounts = { ...currentCounts };
        newCounts[reactionType] = (newCounts[reactionType] || 0) + 1;
        newCounts.total += 1;
        
        // Update user reactions
        const newUserReactions = [...existingUserReactions];
        if (!newUserReactions.includes(reactionType)) {
          newUserReactions.push(reactionType);
        }
        
        state.reactionCounts[entityId] = newCounts;
        state.userReactions[entityId] = newUserReactions;
      });
      
      try {
        // API call - actual server sync
        await api.post(`/reactions`, {
          entityId,
          reactionType
        });
        
        // If successful, update the full state from server
        await get().fetchReactions(entityId);
      } catch (error) {
        // Rollback optimistic update on failure
        set((state) => {
          const currentCounts = state.reactionCounts[entityId] || {};
          const newCounts = { ...currentCounts };
          
          // Revert counts
          newCounts[reactionType] = Math.max(0, (newCounts[reactionType] || 1) - 1);
          newCounts.total = Math.max(0, (newCounts.total || 1) - 1);
          
          // Remove from user reactions
          const newUserReactions = state.userReactions[entityId]?.filter(
            rt => rt !== reactionType
          ) || [];
          
          state.reactionCounts[entityId] = newCounts;
          state.userReactions[entityId] = newUserReactions;
          state.error[entityId] = error.message || 'Failed to add reaction';
        });
      }
    },
    
    removeReaction: async (entityId, reactionType) => {
      const existingUserReactions = get().userReactions[entityId] || [];
      
      // Optimistic update
      set((state) => {
        const currentCounts = state.reactionCounts[entityId] || {};
        const newCounts = { ...currentCounts };
        
        newCounts[reactionType] = Math.max(0, (newCounts[reactionType] || 1) - 1);
        newCounts.total = Math.max(0, (newCounts.total || 1) - 1);
        
        const newUserReactions = existingUserReactions.filter(rt => rt !== reactionType);
        
        state.reactionCounts[entityId] = newCounts;
        state.userReactions[entityId] = newUserReactions;
      });
      
      try {
        await api.delete(`/reactions`, {
          entityId,
          reactionType
        });
        
        await get().fetchReactions(entityId);
      } catch (error) {
        // Rollback on failure
        set((state) => {
          const currentCounts = state.reactionCounts[entityId] || {};
          const newCounts = { ...currentCounts };
          
          newCounts[reactionType] = (newCounts[reactionType] || 0) + 1;
          newCounts.total += 1;
          
          const newUserReactions = [...existingUserReactions, reactionType];
          
          state.reactionCounts[entityId] = newCounts;
          state.userReactions[entityId] = newUserReactions;
          state.error[entityId] = error.message || 'Failed to remove reaction';
        });
      }
    },
    
    fetchReactions: async (entityId) => {
      set((state) => {
        state.isFetching[entityId] = true;
        state.error[entityId] = null;
      });
      
      try {
        const response = await api.get(`/reactions/${entityId}`);
        
        set((state) => {
          state.reactionsByEntity[entityId] = response.data;
          
          // Calculate and normalize counts
          const counts: ReactionCounts = {
            like: 0, love: 0, haha: 0, wow: 0, sad: 0, angry: 0, total: 0
          };
          
          response.data.forEach(reaction => {
            counts[reaction.reactionType] += 1;
            counts.total += 1;
          });
          
          state.reactionCounts[entityId] = counts;
          
          // Extract user reactions
          const userReactionTypes = response.data
            .filter(r => r.userId === getCurrentUserId())
            .map(r => r.reactionType);
          
          state.userReactions[entityId] = userReactionTypes;
        });
      } catch (error) {
        set((state) => {
          state.error[entityId] = error.message || 'Failed to fetch reactions';
        });
      } finally {
        set((state) => {
          state.isFetching[entityId] = false;
        });
      }
    },
    
    clearEntity: (entityId) => {
      set((state) => {
        delete state.reactionsByEntity[entityId];
        delete state.userReactions[entityId];
        delete state.reactionCounts[entityId];
        delete state.isFetching[entityId];
        delete state.error[entityId];
      });
    }
  }))
);

// Custom hook for reaction operations
export const useReactions = (entityId: string) => {
  const {
    reactionsByEntity,
    userReactions,
    reactionCounts,
    isFetching,
    error,
    addReaction,
    removeReaction,
    fetchReactions
  } = useReactionsStore();

  const userHasReacted = (reactionType: string) => 
    userReactions[entityId]?.includes(reactionType) || false;

  const reactionCount = (reactionType: string) => 
    reactionCounts[entityId]?.[reactionType] || 0;

  const totalReactions = reactionCounts[entityId]?.total || 0;

  return {
    reactions: reactionsByEntity[entityId] || [],
    userReactions: userReactions[entityId] || [],
    reactionCounts: reactionCounts[entityId] || {
      like: 0, love: 0, haha: 0, wow: 0, sad: 0, angry: 0, total: 0
    },
    isFetching: isFetching[entityId] || false,
    error: error[entityId],
    addReaction: (reactionType: string) => addReaction(entityId, reactionType),
    removeReaction: (reactionType: string) => removeReaction(entityId, reactionType),
    fetchReactions: () => fetchReactions(entityId),
    userHasReacted,
    reactionCount,
    totalReactions
  };
};

// Types for the reactions system
export interface Reaction {
  id: string;
  userId: string;
  reactionType: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry';
  timestamp: number;
}

export interface ReactionCounts {
  like: number;
  love: number;
  haha: number;
  wow: number;
  sad: number;
  angry: number;
  total: number;
}

The hook provides:

  • Immediate feedback: Optimistic updates for UI responsiveness
  • Server synchronization: Full state management with API integration
  • Error handling: Rollback and user feedback on failures
  • Type safety: Strong typing for all reaction operations
  • Performance optimization: Memoized values and efficient state updates

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    B --> D[/login]
    B --> E[/register]
    B --> F[/explore]
    C --> G[/dashboard]
    C --> H[/posts]
    C --> I[/profile]
    H --> J[/posts/:postId]
    H --> K[/posts/:postId/reactions]

The application will follow a protected route structure with public authentication routes and authenticated content routes. The /posts/:postId/reactions nested route will handle the specific reaction details view for a post.

Code Splitting Strategy

Route-based code splitting:

  • /login and /register - Split into separate bundles to reduce initial load size
  • /dashboard - Split into its own chunk as it's a major feature page
  • /posts/:postId/reactions - Split into its own chunk since it's a specialized view that may not be accessed frequently

Justification:

  • Authentication routes are accessed by all users but don't require heavy dependencies
  • Dashboard and posts views contain complex UI components that benefit from lazy loading
  • Post reactions view is a specialized feature with its own component tree and dependencies
// AuthGuard.tsx
const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const { isAuthenticated, isLoading } = useAuth();
  
  if (isLoading) return <LoadingSpinner />;
  
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return children;
};

// PublicRoute.tsx
const PublicRoute = ({ children }: { children: React.ReactNode }) => {
  const { isAuthenticated } = useAuth();
  
  if (isAuthenticated) {
    return <Navigate to="/dashboard" replace />;
  }
  
  return children;
};

Deep Linking Support and URL Parameter Design

URL structure will follow REST conventions:

  • GET /api/posts/:postId/reactions - Fetch reactions for a specific post
  • POST /api/posts/:postId/reactions - Create or update user's reaction
  • DELETE /api/posts/:postId/reactions - Remove user's reaction

URL parameters will be:

  • postId: Required, numeric identifier for the post
  • reactionType: Optional, string representing reaction type (like, love, etc.)

Browser History Management and Scroll Restoration

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

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

// ScrollRestoration.tsx
const ScrollRestoration = () => {
  const location = useLocation();
  
  useEffect(() => {
    window.scrollTo(0, 0);
  }, [location.pathname]);
  
  return null;
};

The scroll restoration will reset to top on route changes, with specific handling for the reactions view to maintain user context when navigating between posts.

// Route configuration with lazy loading
const routes = [
  {
    path: '/login',
    element: <Login />,
    lazy: () => import('./pages/Login'),
  },
  {
    path: '/register',
    element: <Register />,
    lazy: () => import('./pages/Register'),
  },
  {
    path: '/dashboard',
    element: <Dashboard />,
    lazy: () => import('./pages/Dashboard'),
    guard: AuthGuard,
  },
  {
    path: '/posts/:postId/reactions',
    element: <PostReactions />,
    lazy: () => import('./pages/PostReactions'),
    guard: AuthGuard,
  },
];

5. API Integration Layer

API Client Design Pattern

We'll implement a custom fetch wrapper with Axios as the underlying HTTP client for robust request handling and consistent error management.

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

const createApiClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
    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;
    },
    (error) => Promise.reject(error)
  );

  // Response interceptor for error 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

Request Interceptor:

  • Injects authentication tokens from localStorage
  • Adds request metadata (timestamp, user agent)
  • Handles automatic retry logic for transient errors

Response Interceptor:

  • Normalizes error responses to consistent format
  • Handles automatic re-authentication flow for 401s
  • Logs API errors for monitoring

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) {
    const { status, data } = error.response;
    return new ApiError(
      status,
      data.message || 'API request failed',
      data.code
    );
  }
  
  if (error.request) {
    return new ApiError(503, 'Network error - please check your connection');
  }
  
  return new ApiError(500, 'Unexpected error occurred');
};

// Retry strategy with exponential backoff
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  retries = 3,
  delay = 1000
): Promise<any> => {
  try {
    return await fn();
  } catch (error) {
    if (retries > 0 && [500, 502, 503, 504].includes(error?.response?.status)) {
      await new Promise(resolve => setTimeout(resolve, delay));
      return retryWithBackoff(fn, retries - 1, delay * 2);
    }
    throw error;
  }
};

Loading, Error, and Empty States Design

All API operations will follow a consistent state management pattern:

// types/api.ts
export type ApiState<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: ApiError };

// Example usage in component
const [reactionState, setReactionState] = useState<ApiState<ReactionResponse>>({ status: 'idle' });

Data Fetching Strategy

For a reaction system, we'll use Client-Side Rendering (CSR) with React Query for optimal performance:

  • Primary reactions list: Fetch on component mount
  • Individual reaction creation: Optimistic update with server sync
  • Reaction counts: Cached with automatic invalidation on changes
// hooks/useReactions.ts
import { useQuery, useMutation, QueryClient } from '@tanstack/react-query';

const REACTION_KEYS = {
  LIST: (postId: string) => ['reactions', postId] as const,
  COUNT: (postId: string) => ['reaction-count', postId] as const,
};

export const useReactions = (postId: string) => {
  return useQuery<Reaction[], ApiError>({
    queryKey: REACTION_KEYS.LIST(postId),
    queryFn: () => fetchReactions(postId),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
};

export const useReactionMutation = (postId: string) => {
  const queryClient = new QueryClient();
  
  return useMutation<Reaction, ApiError, ReactionInput>({
    mutationFn: (reaction) => createReaction(postId, reaction),
    onSuccess: (newReaction) => {
      // Optimistic update
      queryClient.setQueryData<Reaction[]>(REACTION_KEYS.LIST(postId), (old) => [
        ...(old || []),
        newReaction,
      ]);
      
      // Update count
      queryClient.setQueryData(REACTION_KEYS.COUNT(postId), (old: number) => 
        (old || 0) + 1
      );
    },
    onError: (error, reactionInput, context) => {
      // Revert optimistic update on failure
      queryClient.invalidateQueries(REACTION_KEYS.LIST(postId));
    },
  });
};

API Response Transformation Layer

// api/transformers.ts
export interface ReactionApiResponse {
  id: string;
  userId: string;
  reactionType: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry';
  timestamp: string;
  user?: {
    id: string;
    name: string;
    avatarUrl?: string;
  };
}

export interface Reaction {
  id: string;
  userId: string;
  type: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry';
  timestamp: Date;
  user?: {
    id: string;
    name: string;
    avatarUrl?: string;
  };
}

export const transformReactionResponse = (
  response: ReactionApiResponse
): Reaction => ({
  id: response.id,
  userId: response.userId,
  type: response.reactionType,
  timestamp: new Date(response.timestamp),
  user: response.user ? {
    id: response.user.id,
    name: response.user.name,
    avatarUrl: response.user.avatarUrl,
  } : undefined,
});

Sequence Diagram for Primary Data Flows

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

    U->>C: Clicks like button
    C->>Q: Mutate reaction
    Q->>A: POST /posts/{id}/reactions
    A->>S: Forward request
    S-->>A: Response
    A-->>Q: Response
    Q->>C: Update UI with optimistic result
    Q->>S: Poll for server sync
    S-->>Q: Confirm update
    Q-->>C: Finalize state

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): < 2.5s for all content types
  • Interaction to Next Paint (INP): < 200ms for user interactions
  • Cumulative Layout Shift (CLS): < 0.1 for all user flows

Bundle Size Budget per Route

  • Post Detail Page: 250KB (including all dependencies)
  • Feed View: 180KB (core components + reactions)
  • Reaction Modal: 80KB (modal + reaction picker)

Critical Rendering Path Optimization

// Critical CSS for reaction button (inline in head)
const criticalReactionStyles = `
  .reaction-button {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 6px 12px;
    border-radius: 16px;
    background: transparent;
    border: 1px solid var(--border-color);
    cursor: pointer;
    transition: all 0.2s ease;
  }
  
  .reaction-button:hover {
    background: var(--hover-bg);
  }
  
  .reaction-count {
    font-size: 14px;
    font-weight: 500;
  }
`;

Image Optimization Strategy

  • Reaction emojis: SVG sprites with lazy loading
  • User avatars: WebP format with responsive sizing (1x, 2x, 3x)
  • Lazy loading: IntersectionObserver with loading="lazy" attribute
  • Preload strategy: Preload popular reaction emojis when user scrolls near reactions

Font Loading Strategy

// Font loading with FOUT mitigation
const fontLoadingStrategy = {
  // Critical font stack for reaction UI
  criticalFontStack: "'Inter', 'Segoe UI', system-ui, sans-serif",
  
  // Preload critical font subsets
  preloadFonts: [
    { href: '/fonts/inter-regular.woff2', as: 'font', type: 'font/woff2' },
    { href: '/fonts/inter-bold.woff2', as: 'font', type: 'font/woff2' }
  ],
  
  // Font display strategy
  fontDisplayStrategy: 'swap'
};

Memoization Strategy

// Reaction count memoization
const ReactionCount = React.memo(({ count, reactionType }: { count: number; reactionType: string }) => {
  const formattedCount = useMemo(() => formatNumber(count), [count]);
  
  return (
    <span className="reaction-count" aria-label={`${count} ${reactionType}s`}>
      {formattedCount}
    </span>
  );
});

// Reaction button memoization
const ReactionButton = React.memo(({ 
  reactionType, 
  count, 
  isActive,
  onClick 
}: {
  reactionType: string;
  count: number;
  isActive: boolean;
  onClick: () => void;
}) => {
  const handleClick = useCallback(() => onClick(), [onClick]);
  
  return (
    <button
      className={`reaction-button ${isActive ? 'active' : ''}`}
      onClick={handleClick}
      aria-pressed={isActive}
      aria-label={`React with ${reactionType}`}
    >
      <span className="emoji">{getEmoji(reactionType)}</span>
      <ReactionCount count={count} reactionType={reactionType} />
    </button>
  );
});

Virtualization for Large Lists

For feeds with thousands of posts, implement virtualization:

// Virtualized reaction list component
const VirtualizedReactionList = ({ reactions }: { reactions: Reaction[] }) => {
  const { height, width } = useWindowSize();
  
  return (
    <div className="virtualized-container">
      <FixedSizeList
        height={height * 0.6}
        width={width * 0.8}
        itemCount={reactions.length}
        itemSize={60}
      >
        {({ index, style }) => (
          <div style={style}>
            <ReactionItem reaction={reactions[index]} />
          </div>
        )}
      </FixedSizeList>
    </div>
  );
};

Prefetching and Preloading Strategy

// Reaction prefetching strategy
const reactionPrefetchStrategy = {
  // Preload popular reactions when user scrolls near feed
  preloadPopularReactions: () => {
    const popularReactions = ['like', 'love', 'laugh', 'wow'];
    
    popularReactions.forEach(reaction => {
      const img = new Image();
      img.src = `/emojis/${reaction}.svg`;
    });
  },
  
  // Preload user's reaction history
  preloadUserReactions: (userId: string) => {
    fetch(`/api/users/${userId}/reactions`)
      .then(response => response.json())
      .then(data => {
        // Cache user reactions for 5 minutes
        localStorage.setItem('user-reactions', JSON.stringify(data));
      });
  },
  
  // Prefetch reaction counts for feed items
  prefetchFeedReactionCounts: (postIds: string[]) => {
    const batchRequests = chunk(postIds, 20);
    
    batchRequests.forEach(batch => {
      fetch('/api/reactions/counts', {
        method: 'POST',
        body: JSON.stringify({ postIds: batch })
      });
    });
  }
};

Performance Monitoring

// Performance tracking for reaction interactions
const trackReactionPerformance = (reactionType: string, duration: number) => {
  if (duration > 100) {
    // Log slow reactions for performance analysis
    console.warn(`Slow reaction interaction: ${reactionType} took ${duration}ms`);
  }
  
  // Send to RUM monitoring
  if (window.RUM) {
    window.RUM.trackEvent('reaction_interaction', {
      reactionType,
      duration,
      timestamp: Date.now()
    });
  }
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. For a like/reaction system, we'll implement:

  1. Component-level boundaries for individual reaction items
  2. Feature-level boundary for the entire reactions container
  3. Global error boundary at the application root
graph TD
    A[App] --> B[ReactionsContainer]
    B --> C[ReactionItem]
    B --> D[ReactionButton]
    C --> E[LikeIcon]
    C --> F[ReactionCount]
    D --> G[ReactionButtonUI]

Global Error Handler Design

A global error handler will capture unhandled errors and provide graceful degradation:

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

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

class ErrorBoundary extends React.Component<React.PropsWithChildren, ErrorBoundaryState> {
  constructor(props: React.PropsWithChildren) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Reaction system error:', error, errorInfo);
    // Report to monitoring service
    this.reportError(error, errorInfo);
  }

  private reportError = (error: Error, errorInfo: React.ErrorInfo) => {
    // Send to Sentry or similar monitoring service
    // Example: Sentry.captureException(error, { contexts: { errorInfo } });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="reaction-error-container">
          <p>Something went wrong with reactions.</p>
          <button onClick={() => window.location.reload()}>
            Reload page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

Offline / Network Failure Handling

The system should gracefully handle network failures with:

  1. Optimistic updates that can be rolled back
  2. Offline queue for failed requests
  3. Retry mechanisms with exponential backoff
// ReactionService.ts
class ReactionService {
  private offlineQueue: ReactionRequest[] = [];
  private retryDelay = 1000;

  async likePost(postId: string, reactionType: ReactionType): Promise<ReactionResponse> {
    try {
      const response = await apiClient.post(`/posts/${postId}/reactions`, {
        reactionType,
      });
      
      return response.data;
    } catch (error) {
      if (this.isNetworkError(error)) {
        // Queue for retry later
        this.offlineQueue.push({
          postId,
          reactionType,
          timestamp: Date.now(),
        });
        
        // Return optimistic update to user immediately
        return this.createOptimisticResponse(postId, reactionType);
      }
      
      throw error;
    }
  }

  private isNetworkError(error: any): boolean {
    return (
      error.code === 'NETWORK_ERROR' ||
      error.name === 'NetworkError' ||
      !error.response
    );
  }

  // Retry failed requests when network is restored
  async processOfflineQueue() {
    const now = Date.now();
    const retryableItems = this.offlineQueue.filter(
      item => now - item.timestamp > this.retryDelay
    );

    for (const item of retryableItems) {
      try {
        await this.likePost(item.postId, item.reactionType);
        // Remove from queue on success
        this.offlineQueue = this.offlineQueue.filter(i => i !== item);
      } catch (error) {
        // Keep in queue for next retry attempt
        console.warn('Failed to retry reaction:', item, error);
      }
    }
  }
}

Graceful Degradation Patterns

When reactions fail to load or save:

  1. Fallback UI: Show default reaction count and disabled buttons
  2. Read-only mode: Allow viewing but prevent interaction
  3. User feedback: Clear error messages with recovery options
// ReactionItem.tsx
interface ReactionItemProps {
  postId: string;
  reactionType: ReactionType;
  count: number;
  userReaction?: ReactionType | null;
  loading?: boolean;
  error?: string;
}

const ReactionItem: React.FC<ReactionItemProps> = ({
  postId,
  reactionType,
  count,
  userReaction,
  loading,
  error,
}) => {
  if (error) {
    return (
      <div className="reaction-item-error">
        <span>⚠️</span>
        <span>Failed to load reactions</span>
      </div>
    );
  }

  return (
    <div className="reaction-item">
      {loading ? (
        <span className="loading-spinner">⏳</span>
      ) : (
        <>
          <ReactionButton 
            postId={postId}
            reactionType={reactionType}
            userReaction={userReaction}
          />
          <span className="reaction-count">{count}</span>
        </>
      )}
    </div>
  );
};

User Feedback on Errors

Implement multiple feedback mechanisms:

  1. Toast notifications for user actions
  2. Inline error messages in reaction buttons
  3. Fallback UI states for failed data
// ToastService.ts
class ToastService {
  static showReactionError(message: string, action?: () => void) {
    toast.error(message, {
      position: "top-right",
      autoClose: 5000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true,
      draggable: true,
    });
  }

  static showReactionSuccess(message: string) {
    toast.success(message, {
      position: "top-right",
      autoClose: 3000,
    });
  }
}

// ReactionButton.tsx
const ReactionButton: React.FC<ReactionButtonProps> = ({
  postId,
  reactionType,
  userReaction,
  loading,
}) => {
  const [error, setError] = useState<string | null>(null);

  const handleReaction = async () => {
    try {
      setError(null);
      await reactionService.likePost(postId, reactionType);
      ToastService.showReactionSuccess("Reaction added!");
    } catch (err) {
      setError("Failed to add reaction. Please try again.");
      ToastService.showReactionError("Failed to add reaction");
    }
  };

  return (
    <button 
      onClick={handleReaction}
      disabled={loading}
      className={`reaction-button ${userReaction === reactionType ? 'active' : ''}`}
    >
      {loading ? "..." : <ReactionIcon type={reactionType} />}
      {error && (
        <span className="error-tooltip">{error}</span>
      )}
    </button>
  );
};

Retry Strategy Implementation

Implement exponential backoff for failed requests:

// RetryManager.ts
class RetryManager {
  static async withRetry<T>(
    operation: () => Promise<T>,
    maxRetries = 3,
    baseDelay = 1000
  ): Promise<T> {
    let lastError: Error;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error as Error;
        
        if (attempt === maxRetries) {
          throw lastError;
        }
        
        // Exponential backoff: 1s, 2s, 4s...
        const delay = baseDelay * Math.pow(2, attempt);
        await this.delay(delay);
      }
    }
    
    throw lastError;
  }

  private static delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Resilience Testing Considerations

  1. Network failure simulation in development
  2. Service degradation testing (mocked API timeouts)
  3. Offline mode testing with service workers
  4. Rate limiting simulation to test retry behavior

Error Recovery Patterns

  1. Automatic retry with exponential backoff
  2. Manual refresh capability for users
  3. Offline persistence of user reactions
  4. State restoration on app restart

The error handling strategy ensures that users can continue interacting with the system even when individual reactions fail, maintaining overall system resilience and providing clear feedback for recovery actions.

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target):

  • Test all interactive states: idle, loading, success, error
  • Verify proper rendering with different reaction types and counts
  • Validate keyboard navigation and ARIA attributes
  • Test edge cases: zero reactions, max reactions, user already reacted
  • Test custom hook behavior in isolation

Utility Tests (80% coverage target):

  • Reaction type validation logic
  • Count normalization and formatting
  • State transition helpers (e.g., calculate new reaction count)
  • Error handling utilities

Integration Testing Strategy

API Interaction Tests (60% coverage target):

  • Test successful like/unlike flows with mocked server responses
  • Validate optimistic update behavior in UI
  • Test concurrent reactions from multiple users
  • Verify proper error recovery and retry logic

State Management Integration (50% coverage target):

  • Test reaction state updates across multiple components
  • Validate cache invalidation when reactions change
  • Verify proper global state synchronization

E2E Testing Scope

Cypress-based E2E Coverage:

  • End-to-end user journey: view content → click like → verify state change
  • Multi-user concurrent reaction scenarios (simulated)
  • Offline mode recovery flow (if implemented)
  • Cross-browser compatibility for reaction UI behavior

Critical User Flows:

  1. Like/unlike content with proper visual feedback
  2. Reaction count updates in real-time (if applicable)
  3. User can undo reactions within reasonable time window
  4. Reaction persistence across page refreshes

Visual Regression Testing

  • Use Percy or Chromatic for capturing reaction UI states
  • Test all reaction types (like, love, laugh, wow, sad, angry)
  • Validate different screen sizes and device orientations
  • Capture loading states and error states

Accessibility Testing Automation

  • Integrate axe-core with Jest for automated A11y checks
  • Test keyboard navigation through reaction buttons
  • Verify proper focus management and trap behavior
  • Validate screen reader compatibility with ARIA labels

Test Data Strategy

Fixtures and Mocks:

  • Create realistic reaction data sets (user IDs, timestamps, reaction types)
  • Implement mock server responses with various status codes (200, 401, 429, 500)
  • Use factory patterns for generating test user profiles and reaction history
  • Implement time-based mocks to simulate concurrent reactions

Test Coverage Matrix:

Test TypeComponentCoverage Target
UnitReactionButton80%
UnitReactionList75%
IntegrationAPI Client60%
E2EUser Flow100%
A11yAll Components100%

Test Environment Configuration:

  • Use Jest with React Testing Library for unit tests
  • Implement test doubles (mocks/spies) for external dependencies
  • Configure proper test database snapshots for integration tests
  • Set up CI pipeline with parallel test execution and coverage reporting

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The Like/Reaction system must meet WCAG 2.1 AA standards:

  • Keyboard Navigation: All reaction options must be fully operable via keyboard (Tab, Enter, Space)
  • Focus Management: Clear visual focus indicators for all interactive elements
  • Screen Reader Support: Proper ARIA labeling and semantic HTML structure
  • Contrast Ratios: Minimum 4.5:1 contrast ratio for text and interactive elements
  • Skip Links: Option to skip reaction controls in content navigation flow
  • Error Handling: Clear error states with accessible feedback mechanisms

Keyboard navigation support

All reaction options must be keyboard-accessible:

// ReactionButton component keyboard support
const ReactionButton = ({ 
  reactionType, 
  isActive, 
  onClick,
  count
}: {
  reactionType: string;
  isActive: boolean;
  onClick: () => void;
  count: number;
}) => {
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      onClick();
    }
  };

  return (
    <button
      role="button"
      aria-label={`React with ${reactionType}`}
      aria-pressed={isActive}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onClick={onClick}
    >
      {reactionType} ({count})
    </button>
  );
};

Focus management strategy

Focus management is critical for accessibility:

  1. Focus Trapping: When reaction modal opens, trap focus within the modal
  2. Focus Restoration: Return focus to trigger element when modal closes
  3. Visual Indicators: Clear focus rings that meet WCAG standards
// Focus management in ReactionModal component
const useFocusTrap = (isOpen: boolean) => {
  const modalRef = useRef<HTMLDivElement>(null);
  const focusableElements = useRef<HTMLElement[]>([]);

  useEffect(() => {
    if (isOpen) {
      const elements = modalRef.current?.querySelectorAll(
        'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
      ) as NodeListOf<HTMLElement>;
      
      focusableElements.current = Array.from(elements);
      focusableElements.current[0]?.focus();
    }
  }, [isOpen]);

  const handleFocusTrap = (e: KeyboardEvent) => {
    if (e.key === 'Tab') {
      const firstElement = focusableElements.current[0];
      const lastElement = focusableElements.current[
        focusableElements.current.length - 1
      ];

      if (e.shiftKey && document.activeElement === firstElement) {
        lastElement?.focus();
        e.preventDefault();
      } else if (!e.shiftKey && document.activeElement === lastElement) {
        firstElement?.focus();
        e.preventDefault();
      }
    }
  };

  return { modalRef, handleFocusTrap };
};

ARIA roles and attributes plan

// Complete ARIA implementation for reaction system
const ReactionSystem = ({ 
  reactions, 
  currentUserReaction,
  onReact
}: {
  reactions: Reaction[];
  currentUserReaction: string | null;
  onReact: (reactionType: string) => void;
}) => {
  return (
    <div 
      role="group" 
      aria-label="Reaction options"
      className="reaction-system"
    >
      {reactions.map((reaction) => (
        <ReactionButton
          key={reaction.type}
          reactionType={reaction.type}
          isActive={currentUserReaction === reaction.type}
          onClick={() => onReact(reaction.type)}
          count={reaction.count}
          aria-describedby={`reaction-${reaction.type}-count`}
        />
      ))}
      
      <div 
        id="reaction-summary"
        role="status" 
        aria-live="polite"
        className="sr-only"
      >
        {currentUserReaction ? 
          `You reacted with ${currentUserReaction}` : 
          'No reaction selected'
        }
      </div>
    </div>
  );
};

Screen reader compatibility

Screen readers must receive clear, contextual information:

  1. Live Regions: Use aria-live="polite" for reaction updates
  2. Descriptive Labels: Each reaction button has descriptive aria-label
  3. Status Updates: Visual feedback for screen reader users when reactions change
  4. Skip Links: Allow keyboard users to skip reaction controls

Focus trapping implementation

// Complete focus trap with proper accessibility
const ReactionModal = ({ 
  isOpen, 
  onClose,
  reactions,
  onReact
}: {
  isOpen: boolean;
  onClose: () => void;
  reactions: Reaction[];
  onReact: (reactionType: string) => void;
}) => {
  const { modalRef, handleFocusTrap } = useFocusTrap(isOpen);
  
  useEffect(() => {
    if (isOpen) {
      document.addEventListener('keydown', handleFocusTrap);
      return () => {
        document.removeEventListener('keydown', handleFocusTrap);
      };
    }
  }, [isOpen, handleFocusTrap]);

  return (
    isOpen && (
      <div 
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="reaction-dialog-title"
        className="reaction-modal"
      >
        <h2 id="reaction-dialog-title" className="sr-only">
          Select a reaction
        </h2>
        
        <div className="reaction-options">
          {reactions.map((reaction) => (
            <button
              key={reaction.type}
              role="option"
              aria-label={`React with ${reaction.type}`}
              onClick={() => {
                onReact(reaction.type);
                onClose();
              }}
            >
              {reaction.emoji} {reaction.type}
            </button>
          ))}
        </div>
        
        <button 
          aria-label="Close reaction dialog"
          onClick={onClose}
          className="close-button"
        >
          ✕
        </button>
      </div>
    )
  );
};

Keyboard accessibility patterns

All interactive elements support:

  1. Tab Navigation: Sequential focus order through reactions
  2. Enter/Space Activation: Buttons respond to keyboard activation
  3. Escape Key: Closes modals and resets focus properly
  4. Arrow Navigation: For keyboard users to navigate reaction options

Semantic HTML structure

// Semantic HTML for reactions
const ReactionSystem = () => {
  return (
    <div className="reaction-container">
      <h3 className="sr-only">Available reactions</h3>
      
      <div role="toolbar" aria-label="Reaction toolbar">
        <button 
          type="button"
          role="button"
          aria-label="Like this post"
          aria-pressed="false"
        >
          👍 12
        </button>
        
        <button 
          type="button"
          role="button"
          aria-label="Love this post"
          aria-pressed="false"
        >
          ❤️ 5
        </button>
        
        <button 
          type="button"
          role="button"
          aria-label="React with laugh"
          aria-pressed="false"
        >
          😂 3
        </button>
      </div>
      
      <div className="reaction-summary">
        <span aria-live="polite" className="sr-only">
          12 people reacted to this post
        </span>
      </div>
    </div>
  );
};

Color contrast compliance

All reaction buttons must maintain:

  • Text contrast: 4.5:1 minimum for all text elements
  • Button states: Hover, focus, and active states maintain sufficient contrast
  • Visual feedback: Clear indication of current selection state

Screen reader announcement patterns

// Live region updates for screen readers
const ReactionSummary = ({ 
  reactions, 
  currentUserReaction,
  totalReactions
}: {
  reactions: Reaction[];
  currentUserReaction: string | null;
  totalReactions: number;
}) => {
  const [announcement, setAnnouncement] = useState('');
  
  useEffect(() => {
    if (currentUserReaction) {
      setAnnouncement(`You reacted with ${currentUserReaction}`);
    } else {
      setAnnouncement(`${totalReactions} reactions`);
    }
  }, [currentUserReaction, totalReactions]);

  return (
    <div 
      role="status" 
      aria-live="polite"
      className="sr-only"
    >
      {announcement}
    </div>
  );
};

10. Security Considerations

XSS Prevention Strategy

The like/reaction system must prevent cross-site scripting attacks through comprehensive input sanitization and output encoding:

// Sanitize reaction data before rendering to prevent XSS
const sanitizeReactionData = (data: string): string => {
  const div = document.createElement('div');
  div.textContent = data;
  return div.innerHTML;
};

// For reaction counts and user names, use DOMPurify for additional safety
import DOMPurify from 'dompurify';

const sanitizeHtml = (html: string): string => {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: [],
    ALLOWED_ATTR: [],
  });
};

CSRF Protection

Implement CSRF token validation for all reaction-related API requests to prevent cross-site request forgery:

// CSRF token management in React context
interface CsrfContextType {
  csrfToken: string | null;
  refreshCsrfToken: () => Promise<void>;
}

const CsrfContext = React.createContext<CsrfContextType>({
  csrfToken: null,
  refreshCsrfToken: async () => {},
});

// In API client, include CSRF token in headers
const apiClient = axios.create({
  baseURL: '/api',
  withCredentials: true,
});

apiClient.interceptors.request.use((config) => {
  const csrfToken = getCsrfToken(); // From context or cookie
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken;
  }
  return config;
});

Content Security Policy (CSP) Headers

Configure CSP headers to restrict reaction-related content execution:

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

Sensitive Data Handling in Frontend State

Reaction system must not store sensitive user data in frontend state:

// Secure reaction state management - only store non-sensitive data
interface Reaction {
  id: string;
  type: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry';
  timestamp: number;
  // Avoid storing user IDs, emails, or PII in client-side state
}

// Use secure storage patterns for reaction preferences
const useReactionPreferences = () => {
  const [preferredReactions, setPreferredReactions] = useState<string[]>([]);
  
  // Only store reaction type preferences, not user identifiers
  useEffect(() => {
    const stored = localStorage.getItem('reactionPreferences');
    if (stored) {
      setPreferredReactions(JSON.parse(stored));
    }
  }, []);

  return { preferredReactions, setPreferredReactions };
};

Input Sanitization Approach

All user-provided reaction data must be sanitized before processing:

// Reaction input validation and sanitization
const validateReactionInput = (input: any): boolean => {
  if (!input || typeof input !== 'object') return false;
  
  const allowedTypes = ['like', 'love', 'haha', 'wow', 'sad', 'angry'];
  const allowedKeys = ['type', 'postId', 'timestamp'];
  
  // Validate reaction type
  if (!allowedTypes.includes(input.type)) return false;
  
  // Validate required fields
  if (!input.postId || typeof input.postId !== 'string') return false;
  
  // Validate timestamp
  if (typeof input.timestamp !== 'number' || input.timestamp < 0) return false;
  
  // Reject any additional unexpected fields
  const keys = Object.keys(input);
  return keys.every(key => allowedKeys.includes(key));
};

// Sanitize reaction submission
const sanitizeReactionSubmission = (reaction: any): any => {
  const sanitized = {
    type: reaction.type,
    postId: reaction.postId,
    timestamp: Date.now(),
  };
  
  // Remove any potentially dangerous properties
  delete sanitized.__proto__;
  delete sanitized.constructor;
  
  return sanitized;
};

Rate Limiting and Abuse Prevention

Implement client-side rate limiting to prevent abuse:

// Reaction rate limiting to prevent spam
const useReactionRateLimit = (maxReactionsPerMinute = 10) => {
  const [reactionCount, setReactionCount] = useState(0);
  const [lastReset, setLastReset] = useState(Date.now());
  
  const canReact = () => {
    const now = Date.now();
    if (now - lastReset > 60000) {
      setReactionCount(0);
      setLastReset(now);
    }
    
    return reactionCount < maxReactionsPerMinute;
  };
  
  const incrementReaction = () => {
    if (canReact()) {
      setReactionCount(prev => prev + 1);
      return true;
    }
    return false;
  };
  
  return { canReact: incrementReaction };
};

Secure API Communication

All reaction-related API communications must use HTTPS and proper authentication:

// Secure API client with proper headers and authentication
const reactionApiClient = axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
  },
});

// Add authentication token to all requests
reactionApiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('authToken');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle authentication errors gracefully
reactionApiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      // Clear auth state and redirect to login
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Token Security

Reaction system must not expose sensitive tokens in client-side code:

// Secure token handling - never log or expose tokens
const secureTokenHandler = {
  // Store tokens in HTTP-only cookies when possible
  setSecureToken: (token: string) => {
    // Use secure cookie storage instead of localStorage
    document.cookie = `authToken=${token}; Secure; HttpOnly; SameSite=Strict`;
  },
  
  // Validate token format before use
  validateToken: (token: string): boolean => {
    if (!token || typeof token !== 'string') return false;
    
    // Basic JWT validation (no actual decoding)
    const parts = token.split('.');
    return parts.length === 3 && 
           parts.every(part => /^[a-zA-Z0-9\-_]+$/g.test(part));
  },
  
  // Remove tokens from memory when no longer needed
  clearToken: () => {
    localStorage.removeItem('authToken');
    document.cookie = 'authToken=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
  }
};

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with feature-scoped keys:

// Translation keys for like/reaction system
{
  "reactions": {
    "like": "Like",
    "love": "Love",
    "laugh": "Laugh",
    "wow": "Wow",
    "sad": "Sad",
    "angry": "Angry"
  },
  "reactionCount": "{{count}} {{reactionType}}",
  "reactionTooltip": "React with {{reactionType}}",
  "reactionsListTitle": "Reactions",
  "reactionsEmpty": "No reactions yet"
}

RTL Support Considerations

RTL layout handling for reaction buttons and counters:

  • Reaction buttons should reverse order in RTL contexts
  • Counters should align right in RTL layouts
  • Emoji reactions may need special handling for RTL directionality

Date, Number, Currency Formatting Strategy

Reaction counts use localized number formatting:

// Example using Intl.NumberFormat for large numbers
const formatter = new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'short'
});

// For "1.2K" format in English, "1.2k" in German
const formattedCount = formatter.format(1200); // "1.2K"

Locale Detection and Switching Strategy

Automatic locale detection from browser settings:

// Auto-detect user's preferred language
const detectedLocale = navigator.language || 'en-US';

// Fallback to English for unsupported languages
const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
const currentLocale = supportedLocales.includes(detectedLocale) 
  ? detectedLocale 
  : 'en-US';

// Manual locale switcher component
const LocaleSwitcher = () => {
  const [currentLocale, setCurrentLocale] = useState('en-US');
  
  const handleLocaleChange = (newLocale: string) => {
    setCurrentLocale(newLocale);
    // Update i18n context and persist preference
  };
};

Translation Implementation Approach

Using react-i18next with fallback strategies:

// i18n configuration
const i18nConfig = {
  supportedLngs: ['en-US', 'fr-FR', 'es-ES', 'de-DE'],
  fallbackLng: 'en-US',
  interpolation: {
    escapeValue: false,
    format: (value, format, lng) => {
      if (format === 'number') {
        return new Intl.NumberFormat(lng).format(value);
      }
      return value;
    }
  }
};

// Translation hook for reactions
const useReactionTranslations = () => {
  const { t } = useTranslation();
  
  return {
    reactionLabels: {
      like: t('reactions.like'),
      love: t('reactions.love'),
      laugh: t('reactions.laugh'),
      wow: t('reactions.wow'),
      sad: t('reactions.sad'),
      angry: t('reactions.angry')
    },
    formatCount: (count: number, reactionType: string) => 
      t('reactionCount', { count, reactionType })
  };
};

Language-Specific Emoji Considerations

Some emojis may have different cultural meanings or availability:

  • Heart emoji (❤️) is generally universal but may appear differently in some locales
  • Cultural context for reactions like "wow" vs. "amazing" may vary by region
  • Emoji rendering consistency across browsers and platforms

Translation Fallback Behavior

  1. Direct translation lookup with fallback to English
  2. Component-level fallback for missing keys
  3. Runtime error logging for untranslated strings
  4. Translation key validation during build process

Performance Considerations

  • Translation bundles split by language for code splitting
  • Preloading translation files for user's detected locale
  • Caching translation results to avoid repeated lookups
  • Lazy loading of translation files when switching languages

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The Like/Reaction system should track the following key RUM metrics:

  • First Input Delay (FID): Target < 100ms for reaction button interactions
  • Largest Contentful Paint (LCP): Target < 2.5s for reaction count updates
  • Cumulative Layout Shift (CLS): Target < 0.1 for reaction button animations
  • Reaction Click Through Rate: Percentage of users who click reactions vs. total interactions
  • Reaction Update Latency: Time from user click to UI update completion

Error Tracking Integration

Sentry Integration Strategy:

  • Capture all reaction API failures with context (user ID, content ID, reaction type)
  • Track optimistic update failures and rollback scenarios
  • Monitor rate limiting errors for reaction endpoints
  • Log client-side validation errors (e.g., invalid reaction types)

Custom Events and User Journey Tracking

Tracking Events:

// Reaction tracking events
interface ReactionEvent {
  event: 'reaction_added' | 'reaction_removed' | 'reaction_count_updated';
  userId: string;
  contentId: string;
  reactionType: ReactionType;
  timestamp: number;
  duration?: number; // Time from click to completion
}

// User journey tracking
interface ReactionJourney {
  userId: string;
  contentId: string;
  reactionSequence: Array<{
    type: ReactionType;
    timestamp: number;
    action: 'add' | 'remove';
  }>;
  totalReactions: number;
  journeyDuration: number;
}

Performance Monitoring and Regression Detection

Performance Baselines:

  • Average reaction API response time: < 200ms (95th percentile)
  • UI update latency: < 150ms for reaction count changes
  • Memory usage per reaction component: < 2KB

Regression Detection Strategy:

  • Set up automated performance tests in CI pipeline
  • Monitor reaction system performance against baseline metrics
  • Alert thresholds: 20% increase in API response times or UI update delays

Monitoring Dashboard Components:

  • Real-time reaction volume dashboard
  • API latency trend charts
  • User engagement heatmaps for reaction types
  • Error rate monitoring with alerting thresholds
graph TD
    A[User Interaction] --> B[Reaction Button Click]
    B --> C[API Request]
    C --> D[Server Processing]
    D --> E[Response]
    E --> F[UI Update]
    F --> G[Performance Metrics]
    G --> H[Sentry Errors]
    G --> I[Real User Monitoring]
    H --> J[Alerting System]
    I --> K[Dashboard Analytics]

Key Metrics to Monitor:

  • Reaction API success rate (target: 99.9%)
  • Concurrent reaction operations (max: 100 simultaneous)
  • Cache hit ratio for reaction data (target: > 85%)
  • Memory leak detection in reaction state management
  • User session duration correlation with reaction engagement

Integration Points:

  • Datadog for infrastructure monitoring
  • Mixpanel for user behavior analytics
  • New Relic for full-stack performance monitoring
  • Grafana dashboards for custom metric visualization

Alerting Strategy:

  • Critical: Reaction API failure rate > 1%
  • Warning: UI update latency > 300ms
  • Info: User engagement drop > 15% in 24h window

13. File & Folder Structure

Proposed Directory Structure

/src
  /features
    /reactions
      /components
        ReactionButton.tsx
        ReactionList.tsx
        ReactionPopover.tsx
      /hooks
        useReaction.ts
        useReactions.ts
      /types
        reaction.types.ts
      /services
        reaction.service.ts
      /store
        reaction.store.ts
      /utils
        reaction.utils.ts
  /shared
    /components
      IconButton.tsx
      Tooltip.tsx
    /hooks
      useDebounce.ts
    /types
      common.types.ts
  /app
    /routes
      PostPage.tsx
      FeedPage.tsx
    /layout
      MainLayout.tsx

Module Boundaries and Import Rules

  • Feature isolation: Reaction components and logic are self-contained within /features/reactions
  • No cross-feature imports: Reactions feature cannot directly import from other features (e.g., posts, comments)
  • Shared components: Only use shared components like IconButton and Tooltip from /shared
  • Barrel exports: Use barrel files for clean imports (e.g., import { ReactionButton } from 'features/reactions')

Shared vs Feature-Specific Code Separation

Shared components (/shared) contain:

  • Reusable UI primitives (buttons, tooltips, modals)
  • Generic utility hooks (debounce, scroll tracking)
  • Common types and interfaces

Feature-specific code (/features/reactions) contains:

  • Reaction-specific UI components
  • Business logic and service integrations
  • Type definitions unique to reactions
  • Custom hooks for reaction state management

This structure enables clear ownership boundaries, supports feature isolation for independent development and testing, and maintains a clean separation between generic UI components and domain-specific functionality. The feature-based organization allows for easy scaling when additional reaction types or engagement features are added.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React Query (TanStack Query)

  • Justification: Excellent for server state management with built-in caching, invalidation, and background updates. Provides automatic revalidation, optimistic updates, and robust error handling out of the box.
  • Bundle Impact: ~15KB gzipped
  • Alternatives Considered: Redux Toolkit, Zustand - React Query's superior caching strategy and built-in SSR support make it the clear winner for this feature.

2. Framer Motion

  • Justification: Provides smooth, performant animations for like/reaction feedback with minimal boilerplate. Perfect for visual confirmation of user actions.
  • Bundle Impact: ~8KB gzipped
  • Alternatives Considered: CSS animations, React Spring - Framer Motion offers the best balance of performance and developer experience for UI feedback.

3. React Hook Form

  • Justification: For any form-based interaction (e.g., custom reaction types), provides optimal performance and validation handling with minimal re-renders.
  • Bundle Impact: ~5KB gzipped
  • Alternatives Considered: Custom form handling - Hook Form's optimized re-render strategy and built-in validation are essential for performance.

4. Axios

  • Justification: Robust HTTP client with interceptors, request/response transformation, and built-in cancellation support. Excellent for API integration.
  • Bundle Impact: ~12KB gzipped
  • Alternatives Considered: Fetch API - Axios provides better error handling, request cancellation, and interceptor support for complex API flows.

5. Emotion (or Styled Components)

  • Justification: For CSS-in-JS with better performance than traditional CSS modules, particularly for dynamic styling of reactions and animated elements.
  • Bundle Impact: ~10KB gzipped
  • Alternatives Considered: CSS Modules, Styled Components - Emotion offers better performance with automatic CSS extraction and better SSR support.

Alternatives Considered and Rejected

Redux Toolkit

  • Rejected: While powerful, overkill for this feature's scope. React Query handles most server state concerns better, and the additional complexity isn't justified.

Zustand

  • Rejected: Simpler than Redux but lacks the sophisticated caching and server state management that React Query provides for this use case.

React Spring

  • Rejected: More complex than Framer Motion for simple feedback animations, with higher bundle size and more boilerplate.

Jotai

  • Rejected: While excellent for granular state management, not needed for this feature's scope and would add unnecessary complexity.

Licensing and Bundle Size Impact Assessment

Bundle Size Budget:

  • Total frontend dependencies: ~50KB gzipped (excluding React ecosystem)
  • Critical path: 25KB gzipped for core functionality
  • Optimized with tree-shaking and code splitting

Licensing: All selected libraries are MIT licensed with permissive usage terms suitable for enterprise applications. No GPL or restrictive licenses in the dependency chain.

Tree-shaking Strategy: All libraries support tree-shaking with minimal impact on bundle size when properly configured. React Query and Framer Motion are particularly well-optimized for this.

Performance Impact: All selected libraries have minimal runtime overhead and are optimized for production builds with proper bundling strategies.

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Reaction types and thresholds: Should we support custom reaction types (like, love, laugh, wow) or limit to standard emojis? How many reactions should we support before performance impacts become significant?
  • Reaction persistence strategy: Should reactions persist across sessions or be session-only? How do we handle reaction history when users change devices?
  • Real-time vs. batched updates: Should reactions update in real-time or batched for performance? What's the acceptable latency for reaction visibility?

Known trade-offs made in this design

  • State consistency vs. performance: Optimistic updates improve perceived performance but risk inconsistency if server operations fail. We've chosen to implement optimistic updates with proper rollback mechanisms.
  • Code size vs. reusability: Creating highly generic reaction components increases code size but improves reusability across features. We've balanced this with feature-specific optimizations.
  • User experience vs. server load: Real-time reaction updates provide better UX but increase server load and WebSocket connections. We've implemented rate limiting and efficient update strategies.

Risks and mitigations

  • Rate limiting abuse: Users could spam reactions to overwhelm backend systems. Mitigation: Implement client-side rate limiting and server-side throttling with exponential backoff.
  • Data synchronization issues: Race conditions during concurrent reactions could cause data inconsistency. Mitigation: Use optimistic updates with proper server acknowledgment and rollback handling.
  • Performance degradation: Large reaction lists could cause rendering lag. Mitigation: Implement virtualization for reaction lists and lazy loading of reaction details.
  • Memory leaks: Reaction state accumulation over time. Mitigation: Implement proper cleanup of reaction state when components unmount and set TTLs for cached reactions.

Technical debt acknowledged

  • Reaction type extensibility: Current design assumes fixed reaction types, making it harder to add new reaction types without breaking changes. Future refactoring should support dynamic reaction type configuration.
  • Cross-feature coupling: Reaction components are currently tightly coupled to specific content types (posts, comments). This creates maintenance overhead when extending to new content types.
  • Testing coverage gaps: Complex state transitions (e.g., optimistic update rollback) are difficult to unit test comprehensively. This creates potential for edge-case bugs in production.
  • Accessibility compliance: ARIA implementation is basic and may not cover all screen reader use cases. Further accessibility auditing is required for production readiness.
  • Performance monitoring: While we've designed for performance, we haven't fully implemented comprehensive RUM tracking for reaction-specific metrics like reaction click latency or update propagation delays.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend HLD

    Real-Time State Synchronization System Design

    Comprehensive frontend architecture for building scalable real-time state synchronization across distributed client applications with conflict resolution and offline support.

  • Frontend HLD

    Online Food Delivery Platform Frontend Architecture

    Comprehensive high-level design for a scalable food delivery frontend with real-time order tracking and restaurant discovery features.

  • Frontend HLD

    Frontend Code Splitting and Lazy Loading Strategy Design

    Comprehensive high-level design for implementing efficient code splitting and lazy loading in modern web applications to optimize performance and user experience.

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.