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
- What types of reactions are supported (like, love, haha, wow, sad, angry) or should this be extensible?
- Should reactions be tied to specific content types (posts, comments, media) or generic across all content?
- Do we need to support real-time updates for reactions (e.g., live counters, instant reaction display)?
- Should reactions persist offline and sync when connectivity is restored?
- What's the expected scale of reactions (e.g., millions of reactions per content item)?
- Are there any accessibility requirements beyond basic keyboard navigation and screen reader support?
- 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 interactionsReactionButton- Interactive button that toggles reactionsReactionList- Displays all reactions for an item
-
Dumb Components (Presentational):
ReactionCount- Displays reaction countsReactionItem- Individual reaction display with avatar and timestampReactionModal- Modal showing all users who reactedReactionUserList- 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:
- Modular Design: Each component has a single responsibility and can be reused in different contexts
- Configurable Props: Components accept configuration through props to adapt to various use cases
- Type Safety: Strong TypeScript contracts ensure safe reuse across different parts of the application
- 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:
- Simplicity: Zero dependencies, minimal boilerplate
- Flexibility: Easy to integrate with React Query for server state
- TypeScript support: Excellent type inference and safety
- 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
- Entity-level cache invalidation: When reactions change for an entity, invalidate that specific entity's cache
- Timestamp-based invalidation: Use server timestamps to determine freshness
- Manual refresh triggers: Allow users to manually refresh reactions
Optimistic updates design
Optimistic updates are critical for user experience:
- Immediate UI response: Update counts and user reactions instantly
- Rollback on failure: Revert changes if server request fails
- 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:
/loginand/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
Navigation Guards and Authentication Flows
// 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 postPOST /api/posts/:postId/reactions- Create or update user's reactionDELETE /api/posts/:postId/reactions- Remove user's reaction
URL parameters will be:
postId: Required, numeric identifier for the postreactionType: 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:
- Component-level boundaries for individual reaction items
- Feature-level boundary for the entire reactions container
- 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:
- Optimistic updates that can be rolled back
- Offline queue for failed requests
- 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:
- Fallback UI: Show default reaction count and disabled buttons
- Read-only mode: Allow viewing but prevent interaction
- 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:
- Toast notifications for user actions
- Inline error messages in reaction buttons
- 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
- Network failure simulation in development
- Service degradation testing (mocked API timeouts)
- Offline mode testing with service workers
- Rate limiting simulation to test retry behavior
Error Recovery Patterns
- Automatic retry with exponential backoff
- Manual refresh capability for users
- Offline persistence of user reactions
- 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:
- Like/unlike content with proper visual feedback
- Reaction count updates in real-time (if applicable)
- User can undo reactions within reasonable time window
- 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 Type | Component | Coverage Target |
|---|---|---|
| Unit | ReactionButton | 80% |
| Unit | ReactionList | 75% |
| Integration | API Client | 60% |
| E2E | User Flow | 100% |
| A11y | All Components | 100% |
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:
- Focus Trapping: When reaction modal opens, trap focus within the modal
- Focus Restoration: Return focus to trigger element when modal closes
- 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:
- Live Regions: Use
aria-live="polite"for reaction updates - Descriptive Labels: Each reaction button has descriptive
aria-label - Status Updates: Visual feedback for screen reader users when reactions change
- 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:
- Tab Navigation: Sequential focus order through reactions
- Enter/Space Activation: Buttons respond to keyboard activation
- Escape Key: Closes modals and resets focus properly
- 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
- Direct translation lookup with fallback to English
- Component-level fallback for missing keys
- Runtime error logging for untranslated strings
- 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
IconButtonandTooltipfrom/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.