This document presents a complete low-level design for a modern Todo application architecture, covering component structure, state management, performance optimizations, and scalable implementation patterns for enterprise-level frontend development.
1. Problem Statement & Requirements
Clarifying questions
- What target browsers/devices are we supporting? (e.g., mobile-first, IE11 support, touch vs. mouse interactions)
- Do we need authentication/authorization? Will users have accounts and persist todos across sessions?
- Should todo items be real-time synchronized across devices, or is this a local-only app?
- Is offline support required? If so, what level of persistence (localStorage, service workers)?
- What accessibility standards must this meet? (e.g., WCAG 2.1 AA, screen reader compatibility)
- Do we need internationalization support for multiple languages and RTL layouts?
- Are there existing design systems or component libraries we must integrate with?
Functional requirements
- Users can create new todo items with text content
- Users can mark todos as complete/incomplete
- Users can delete existing todos
- Users can filter todos (all, active, completed)
- Users can edit existing todo text content
- Users can clear all completed todos at once
- Todos persist across browser sessions (local storage or backend)
- Users can see a count of remaining active todos
- Multi-select and bulk operations (if time permits)
Non-functional requirements
- Performance: LCP < 2.5s, INP < 200ms, CLS < 0.1
- Accessibility: Full keyboard navigation, ARIA roles, screen reader compatible
- Scalability: Handle up to 10,000 todos efficiently
- Responsiveness: Mobile-first design with touch-friendly interactions
- Reliability: Graceful error handling and offline support
Constraints and assumptions
- Single-user application (no multi-user collaboration)
- No external authentication system required
- Todos are stored locally in browser's localStorage (unless real-time sync is requested)
- No third-party integrations or external APIs
- Component-based architecture with React and TypeScript
- No build-time optimizations (e.g., SSR, SSG) required
Out of scope
- Multi-user todo lists or sharing functionality
- Rich text content in todos (only plain text)
- Advanced filtering (e.g., due dates, tags, priorities)
- Push notifications or real-time sync across devices
- Export/import functionality for todo lists
- Complex animations or transitions (focus on performance)
- Server-side rendering or static site generation
2. Component Architecture
graph TD
A[TodoApp] --> B[Header]
A --> C[MainContent]
C --> D[TodoList]
D --> E[TodoItem]
E --> F[Checkbox]
E --> G[TextInput]
E --> H[DeleteButton]
C --> I[AddTodoForm]
I --> J[InputField]
I --> K[AddButton]
A --> L[Footer]
Component Tree Analysis
Smart Components (Container Components):
TodoApp- Main application container managing global state and routingMainContent- Wraps todo list and form, handles CRUD operationsTodoList- Manages list rendering and filtering logic
Dumb Components (Presentational Components):
Header- Displays app title and brandingTodoItem- Renders individual todo with interactive controlsAddTodoForm- Handles new todo creationFooter- Displays app metadata and statistics
TypeScript Props Contracts
// TodoItem Component
interface TodoItemProps {
id: string;
text: string;
completed: boolean;
onToggle: (id: string) => void;
onEdit: (id: string, newText: string) => void;
onDelete: (id: string) => void;
}
// TodoList Component
interface TodoListProps {
todos: TodoItem[];
onToggle: (id: string) => void;
onEdit: (id: string, newText: string) => void;
onDelete: (id: string) => void;
filter: 'all' | 'active' | 'completed';
}
// AddTodoForm Component
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Editing : StartEdit
Editing --> Idle : SaveEdit
Editing --> Idle : CancelEdit
Idle --> Deleting : StartDelete
Deleting --> Idle : ConfirmDelete
Deleting --> Idle : CancelDelete
Idle --> Adding : AddTodo
Adding --> Idle : TodoAdded
Component Responsibilities
TodoApp:
- Manages global application state and routing
- Handles authentication context (if applicable)
- Coordinates between UI components
MainContent:
- Orchestrates todo list and form interactions
- Implements filtering logic
- Handles CRUD operations for todos
TodoList:
- Renders list of todo items
- Applies filtering and sorting logic
- Delegates item interactions to individual components
TodoItem:
- Renders single todo with interactive controls
- Handles inline editing and deletion
- Manages local UI state for edit mode
AddTodoForm:
- Collects new todo text input
- Validates and submits new todos
- Resets form after successful submission
Reusability Decisions
- TodoItem is highly reusable across different todo contexts
- AddTodoForm can be reused in other list-based applications
- Header/Footer components are likely to be reused across app sections
- TextInput/Checkbox components are generic and reusable
Component Lifecycle Considerations
- Mounting: TodoApp initializes state, loads todos from localStorage
- Updating: TodoList re-renders when todos change or filter updates
- Unmounting: Cleanup of event listeners and timers (if any)
- Error Boundaries: TodoList should wrap individual items for error isolation
- Performance: Memoization of TodoItem components based on todo ID
State Management Integration
TodoItem and TodoList components will consume state from TodoApp's context or store, with appropriate memoization to prevent unnecessary re-renders when only one item changes.
3. State Management Design
Local state vs. global state decisions
For a Todo application, we'll use a hybrid approach:
- Local state: Individual todo item editing, form validation states, and UI-specific toggles (like edit mode)
- Global state: Todo list persistence, filtering states, and user preferences (theme, view mode)
- Server state: Todo items fetched from backend API
State management approach
We'll use Zustand for state management because:
- Minimal boilerplate with intuitive API
- Excellent TypeScript support with strict typing
- No provider wrapping needed - direct store access
- Built-in middleware support for logging and persistence
- Good developer experience for complex state transitions
State shape and normalization strategy
// Normalized state structure for optimal performance
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
}
interface TodoState {
todos: Record<string, Todo>; // Normalized by ID
activeFilter: 'all' | 'active' | 'completed';
loading: boolean;
error: string | null;
editingId: string | null;
newTodoTitle: string;
}
interface UserPreferences {
theme: 'light' | 'dark';
viewMode: 'list' | 'grid';
}
Server state vs. client state separation
// Client state (UI and local operations)
const clientState = {
todos: { ... }, // In-memory normalized todos
activeFilter: 'all',
loading: false,
error: null,
editingId: null,
newTodoTitle: '',
};
// Server state (persisted data)
const serverState = {
todos: { ... }, // Fetched from API, may be stale
lastSyncedAt: Date,
};
Cache invalidation strategy
- Optimistic updates: Apply changes immediately to local state
- Server sync: Revalidate from server after 5 seconds or on explicit refresh
- Manual invalidation: Clear cache when user explicitly marks todos as complete
Optimistic updates design
When a todo is marked complete:
- Immediately update local state
- Send to server asynchronously
- If server fails, revert local state and show error toast
Critical-path hook implementation
// src/hooks/useTodoStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
}
interface TodoStore {
todos: Record<string, Todo>;
activeFilter: 'all' | 'active' | 'completed';
loading: boolean;
error: string | null;
editingId: string | null;
newTodoTitle: string;
// Actions
addTodo: (title: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
updateTodo: (id: string, title: string) => void;
setFilter: (filter: 'all' | 'active' | 'completed') => void;
startEditing: (id: string) => void;
cancelEditing: () => void;
setNewTodoTitle: (title: string) => void;
clearError: () => void;
// Server sync
syncTodos: () => Promise<void>;
}
const useTodoStore = create<TodoStore>()(
persist(
(set, get) => ({
todos: {},
activeFilter: 'all',
loading: false,
error: null,
editingId: null,
newTodoTitle: '',
// CRUD operations with optimistic updates
addTodo: (title) => {
const newTodo: Todo = {
id: Date.now().toString(),
title,
completed: false,
createdAt: new Date(),
updatedAt: new Date(),
};
set((state) => ({
todos: { ...state.todos, [newTodo.id]: newTodo },
newTodoTitle: '',
}));
},
toggleTodo: (id) => {
set((state) => ({
todos: {
...state.todos,
[id]: {
...state.todos[id],
completed: !state.todos[id].completed,
updatedAt: new Date(),
},
},
}));
},
deleteTodo: (id) => {
set((state) => {
const newTodos = { ...state.todos };
delete newTodos[id];
return { todos: newTodos };
});
},
updateTodo: (id, title) => {
set((state) => ({
todos: {
...state.todos,
[id]: {
...state.todos[id],
title,
updatedAt: new Date(),
},
},
editingId: null,
}));
},
setFilter: (filter) => {
set({ activeFilter: filter });
},
startEditing: (id) => {
set({ editingId: id });
},
cancelEditing: () => {
set({ editingId: null });
},
setNewTodoTitle: (title) => {
set({ newTodoTitle: title });
},
clearError: () => {
set({ error: null });
},
// Server sync (example implementation)
syncTodos: async () => {
set({ loading: true });
try {
// Simulate API call
const response = await fetch('/api/todos');
const serverTodos = await response.json();
set({
todos: serverTodos.reduce((acc, todo) => {
acc[todo.id] = todo;
return acc;
}, {} as Record<string, Todo>),
loading: false,
});
} catch (error) {
set({
error: 'Failed to sync todos',
loading: false,
});
}
},
}),
{
name: 'todo-storage',
partialize: (state) => ({
todos: state.todos,
activeFilter: state.activeFilter,
}),
}
)
);
export default useTodoStore;
// Usage in component
const TodoList = () => {
const { todos, activeFilter, loading } = useTodoStore();
const filteredTodos = Object.values(todos).filter(todo => {
if (activeFilter === 'active') return !todo.completed;
if (activeFilter === 'completed') return todo.completed;
return true;
});
if (loading) return <div>Loading...</div>;
return (
<ul>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
};
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Home]
A --> C[Login]
A --> D[Protected]
D --> E[Todos]
D --> F[Profile]
The application will have a protected route structure with:
/- Public home page (no authentication required)/login- Authentication page (public, redirects to todos if authenticated)/todos- Main todo management interface (protected)/profile- User profile page (protected)
Code Splitting Strategy
Route-based code splitting with React.lazy and Suspense:
// routes.tsx
import { lazy, Suspense } from 'react';
import { RouteObject } from 'react-router-dom';
const Home = lazy(() => import('../pages/Home'));
const Login = lazy(() => import('../pages/Login'));
const Todos = lazy(() => import('../pages/Todos'));
const Profile = lazy(() => import('../pages/Profile'));
export const routes: RouteObject[] = [
{
path: '/',
element: (
<Suspense fallback={<div>Loading...</div>}>
<Home />
</Suspense>
),
},
{
path: '/login',
element: (
<Suspense fallback={<div>Loading...</div>}>
<Login />
</Suspense>
),
},
{
path: '/todos',
element: (
<ProtectedRoute>
<Suspense fallback={<div>Loading...</div>}>
<Todos />
</Suspense>
</ProtectedRoute>
),
},
{
path: '/profile',
element: (
<ProtectedRoute>
<Suspense fallback={<div>Loading...</div>}>
<Profile />
</Suspense>
</ProtectedRoute>
),
},
];
Justification:
- Route-based splitting ensures that protected components load only when needed
- Login and Home pages are small and can be loaded eagerly
- Todos and Profile pages contain complex UIs that benefit from lazy loading
Navigation Guards and Authentication Flows
// ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
Deep Linking Support and URL Parameter Design
URL structure:
/todos- Main todo list view/todos?filter=all|active|completed- Filter todos by status/todos/:id- View specific todo details (if implemented)/todos?sort=date|priority- Sort todos
// URL parameter handling in Todos component
const TodoList: React.FC = () => {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const filter = searchParams.get('filter') || 'all';
const sort = searchParams.get('sort') || 'date';
// Apply filters and sorting to todo data
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
const sortedTodos = [...filteredTodos].sort((a, b) => {
if (sort === 'priority') {
return a.priority - b.priority;
}
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
});
return (
<div>
<TodoFilter filter={filter} sort={sort} />
{sortedTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
};
Browser History Management and Scroll Restoration
// App.tsx
import { BrowserRouter, useScrollRestoration } from 'react-router-dom';
const App: React.FC = () => {
return (
<BrowserRouter>
<ScrollRestoration />
<Routes>
{routes}
</Routes>
</BrowserRouter>
);
};
// Custom scroll restoration hook
const ScrollRestoration: React.FC = () => {
const location = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [location.pathname]);
return null;
};
The application will use React Router's built-in scroll restoration with custom handling for route transitions to ensure consistent user experience across navigation.
5. API Integration Layer
API Client Design Pattern
We'll implement a custom fetch wrapper with axios as the underlying HTTP client to provide consistent request handling and error management across the application.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:3000/api';
const createApiClient = (): AxiosInstance => {
const client = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding auth token
client.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for handling errors
client.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
return client;
};
export const apiClient = createApiClient();
Request/Response Interceptors
The client handles authentication token injection, response normalization, and error transformation:
- Authentication: Automatically includes Bearer token in all requests
- Response Normalization: Ensures consistent response structure across endpoints
- Error Transformation: Converts HTTP errors to standardized error objects
Error Handling and Retry Strategy
We implement a retry mechanism with exponential backoff for transient network errors:
// api/errorHandler.ts
import { AxiosError } from 'axios';
export interface ApiError {
status: number;
message: string;
code?: string;
}
export const handleApiError = (error: AxiosError): ApiError => {
if (error.response) {
return {
status: error.response.status,
message: error.response.data?.message || 'API Error',
code: error.response.data?.code,
};
}
if (error.request) {
return {
status: 0,
message: 'Network Error - Unable to reach server',
};
}
return {
status: 500,
message: error.message || 'Unknown Error',
};
};
export const shouldRetry = (error: AxiosError): boolean => {
const status = error.response?.status;
return (
(status === 408 || status === 429 || status === 500 || status === 503) ||
(status === 401 && !localStorage.getItem('authToken'))
);
};
Loading, Error, and Empty States Design
We implement a standardized state management approach for API operations:
// api/types.ts
export type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: ApiError };
export type Todo = {
id: string;
title: string;
completed: boolean;
createdAt: string;
updatedAt: string;
};
export type TodoListResponse = {
todos: Todo[];
total: number;
page: number;
limit: number;
};
Data Fetching Strategy
For a Todo application, we use CSR (Client-Side Rendering) with React Query for optimal data fetching:
- Primary Route:
/todos- Fetches all todos with pagination - Secondary Routes: Individual todo operations (create, update, delete)
- Caching Strategy:
- Cache results for 5 minutes
- Invalidate cache on mutations
- Background refetching for stale data
API Response Transformation Layer
We transform raw API responses into domain models:
// api/transformers.ts
import { Todo, TodoListResponse } from './types';
export const transformTodo = (raw: any): Todo => ({
id: raw.id,
title: raw.title,
completed: raw.completed,
createdAt: new Date(raw.createdAt).toISOString(),
updatedAt: new Date(raw.updatedAt).toISOString(),
});
export const transformTodoListResponse = (raw: any): TodoListResponse => ({
todos: raw.todos.map(transformTodo),
total: raw.total,
page: raw.page,
limit: raw.limit,
});
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant U as User
participant C as TodoComponent
participant Q as ReactQuery
participant A as ApiClient
participant S as Server
U->>C: Fetch Todos
C->>Q: Query todos
Q->>A: GET /todos
A->>S: Request
S->>A: Response
A->>Q: Process response
Q->>C: Update UI with data
C->>U: Display todos
U->>C: Add Todo
C->>Q: Mutate createTodo
Q->>A: POST /todos
A->>S: Request
S->>A: Response
A->>Q: Invalidate cache
Q->>C: Update UI with new todo
C->>U: Display new todo
API Endpoints
GET /api/todos- Fetch todos with paginationPOST /api/todos- Create new todoPUT /api/todos/:id- Update todoDELETE /api/todos/:id- Delete todo
Error Boundaries and Retry Logic
// components/ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Todo API Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Something went wrong with the Todo API</h2>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for all routes
- Interaction to Next Paint (INP): <200ms for user interactions
- Cumulative Layout Shift (CLS): <0.1 for all pages
Bundle Size Budget per Route
- Home Page: <500KB (including all dependencies)
- Todo List View: <300KB
- Todo Detail View: <200KB
Critical Rendering Path Optimization
// Critical CSS extraction and inlining for main view
const criticalCSS = `
.todo-container { display: flex; flex-direction: column; }
.todo-item { padding: 12px; border-bottom: 1px solid #eee; }
.todo-input { padding: 8px; border: 1px solid #ccc; }
`;
// Preload critical resources
const preloadCriticalResources = () => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/critical.css';
document.head.appendChild(link);
};
Image Optimization Strategy
- Formats: WebP for modern browsers, JPEG fallback for legacy
- Lazy Loading:
loading="lazy"attribute on all images - Responsive Images:
srcsetwith multiple sizes for different breakpoints
Font Loading Strategy
// Font loading with font-display: swap
const preloadFonts = () => {
const fontLinks = [
{ href: '/fonts/inter-regular.woff2', weight: '400' },
{ href: '/fonts/inter-bold.woff2', weight: '700' }
];
fontLinks.forEach(font => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.crossOrigin = 'anonymous';
link.href = font.href;
link.setAttribute('data-font-weight', font.weight);
document.head.appendChild(link);
});
};
Memoization Strategy
// Memoize expensive computations in TodoItem component
const TodoItem = React.memo(({ todo, onToggle, onDelete }: TodoItemProps) => {
const isCompleted = React.useMemo(() => todo.completed, [todo.completed]);
return (
<div className={`todo-item ${isCompleted ? 'completed' : ''}`}>
<input
type="checkbox"
checked={isCompleted}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.title}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
// Memoize filtered todo lists
const useFilteredTodos = (todos: Todo[], filter: FilterType) => {
return React.useMemo(() => {
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
}, [todos, filter]);
};
Virtualization for Large Lists
// Using react-window for efficient list rendering
import { FixedSizeList as List } from 'react-window';
const TodoList = ({ todos, onToggle, onDelete }: TodoListProps) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<TodoItem
todo={todos[index]}
onToggle={onToggle}
onDelete={onDelete}
/>
</div>
);
return (
<List
height={400}
itemCount={todos.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
};
Prefetching and Preloading Strategy
// Route-based prefetching for next page navigation
const usePrefetch = (route: string) => {
React.useEffect(() => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = route;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [route]);
};
// Prefetch next page when user is idle
const prefetchNextPage = () => {
if ('serviceWorker' in navigator && 'connection' in navigator) {
const connection = navigator['connection'];
if (connection.effectiveType === '4g') {
// Prefetch next page for high-speed connections
fetch('/api/todos?limit=20&page=2');
}
}
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed at key levels in the component tree to prevent cascading failures and provide graceful degradation:
graph TD
A[App Root] --> B[ErrorBoundary]
B --> C[TodoListContainer]
C --> D[TodoItem]
C --> E[AddTodoForm]
B --> F[UserPreferences]
- Application-level boundary: Wrap the entire app to catch unhandled errors and display a generic fallback
- Feature-level boundaries: Wrap TodoListContainer to handle list rendering errors without breaking the entire UI
- Component-level boundaries: Wrap individual TodoItem for isolated error recovery
Global Error Handler Design
Implement a centralized error handler that logs errors and displays user-friendly messages:
// src/core/errorHandler.ts
import { captureException } from '@sentry/react';
export interface ErrorHandlerConfig {
logToConsole?: boolean;
showUserFriendlyMessage?: boolean;
retryAttempts?: number;
}
export class GlobalErrorHandler {
private static instance: GlobalErrorHandler;
private config: ErrorHandlerConfig;
private constructor(config: ErrorHandlerConfig = {}) {
this.config = {
logToConsole: true,
showUserFriendlyMessage: true,
retryAttempts: 3,
...config
};
}
static getInstance(config?: ErrorHandlerConfig): GlobalErrorHandler {
if (!GlobalErrorHandler.instance) {
GlobalErrorHandler.instance = new GlobalErrorHandler(config);
}
return GlobalErrorHandler.instance;
}
handleError(error: Error, context?: string): void {
if (this.config.logToConsole) {
console.error(`[ERROR] ${context || 'Unknown context'}:`, error);
}
captureException(error, {
contexts: {
userContext: { context },
extra: { timestamp: Date.now() }
}
});
if (this.config.showUserFriendlyMessage) {
this.displayUserFriendlyError(error, context);
}
}
private displayUserFriendlyError(error: Error, context?: string): void {
// Implementation for toast notifications or inline error displays
const message = context
? `Failed to ${context}. Please try again.`
: 'An unexpected error occurred. Please refresh the page.';
// Could integrate with toast library like react-hot-toast
console.warn(message);
}
}
Offline / Network Failure Handling
Implement offline detection and resilient UI patterns:
// src/hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';
export const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return { isOnline };
};
// src/components/OfflineIndicator.tsx
import React from 'react';
import { useNetworkStatus } from '../hooks/useNetworkStatus';
interface OfflineIndicatorProps {
className?: string;
}
export const OfflineIndicator: React.FC<OfflineIndicatorProps> = ({
className = ''
}) => {
const { isOnline } = useNetworkStatus();
if (isOnline) return null;
return (
<div className={`offline-indicator ${className}`}>
<span className="offline-icon">⚠️</span>
<span>Offline - Changes will sync when connection is restored</span>
</div>
);
};
Graceful Degradation Patterns
Implement fallback UIs for critical features:
// src/components/TodoListFallback.tsx
import React from 'react';
interface TodoListFallbackProps {
error?: Error;
retry?: () => void;
}
export const TodoListFallback: React.FC<TodoListFallbackProps> = ({
error,
retry
}) => {
return (
<div className="todo-list-fallback">
<h3>Could not load todos</h3>
{error && (
<p className="error-message">
{error.message || 'Failed to load todos'}
</p>
)}
<button
onClick={retry}
className="retry-button"
aria-label="Retry loading todos"
>
Retry
</button>
</div>
);
};
User Feedback on Errors
Implement comprehensive error feedback mechanisms:
// src/components/ErrorToast.tsx
import React from 'react';
interface ErrorToastProps {
message: string;
onClose: () => void;
autoClose?: boolean;
}
export const ErrorToast: React.FC<ErrorToastProps> = ({
message,
onClose,
autoClose = true
}) => {
React.useEffect(() => {
if (autoClose) {
const timer = setTimeout(onClose, 5000);
return () => clearTimeout(timer);
}
}, [autoClose, onClose]);
return (
<div className="error-toast" role="alert">
<span className="error-icon">❌</span>
<span className="error-message">{message}</span>
<button
onClick={onClose}
className="close-button"
aria-label="Close error message"
>
✕
</button>
</div>
);
};
Error Boundary Component Implementation
// src/components/ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ComponentType<{
error?: Error;
resetErrorBoundary?: () => void;
}>;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
this.setState({ errorInfo });
// Log to external monitoring service
console.error('ErrorBoundary caught:', error, errorInfo);
}
resetErrorBoundary = (): void => {
this.setState({ hasError: false, error: undefined });
};
render(): React.ReactNode {
const { hasError, error } = this.state;
const { children, fallback: FallbackComponent } = this.props;
if (hasError) {
if (FallbackComponent) {
return <FallbackComponent error={error} resetErrorBoundary={this.resetErrorBoundary} />;
}
return (
<div className="error-boundary-fallback">
<h2>Something went wrong</h2>
<p>We're sorry, but something went wrong loading this content.</p>
<button onClick={this.resetErrorBoundary}>
Try Again
</button>
</div>
);
}
return children;
}
}
Retry Mechanism Implementation
// src/hooks/useRetryableFetch.ts
import { useState, useCallback } from 'react';
interface RetryableFetchOptions {
retries?: number;
delay?: number;
}
export const useRetryableFetch = <T>({
retries = 3,
delay = 1000
}: RetryableFetchOptions = {}) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<T | null>(null);
const fetchWithRetry = useCallback(async (url: string) => {
setLoading(true);
setError(null);
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
setData(result);
setLoading(false);
return result;
} catch (err) {
if (attempt === retries) {
setError(err as Error);
setLoading(false);
throw err;
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, attempt)));
}
}
}, [retries, delay]);
return {
data,
loading,
error,
refetch: fetchWithRetry
};
};
8. Testing Strategy
Unit Testing Approach
- Component-level testing: 90%+ coverage for all presentational and container components using React Testing Library
- Utility function testing: 100% coverage for all custom hooks, validation functions, and data transformation utilities
- Test pyramid: 70% unit tests, 20% integration tests, 10% E2E tests
- Test data strategy: Use fixtures with realistic but minimal test data to avoid brittle tests
Integration Testing Strategy
- Component composition: Test interactions between connected components (e.g., TodoList → TodoItem)
- State management integration: Verify custom hooks properly integrate with global state (Zustand store)
- API layer integration: Test component behavior with mocked API responses to ensure proper loading/error states
- Form submission flows: Validate multi-step form handling and state transitions
E2E Testing Scope
- Primary user flows:
- Add new todos with validation
- Mark todos complete/incomplete
- Edit existing todos
- Delete todos
- Filter todos by status
- Tool choice: Playwright for cross-browser testing (Chrome, Firefox, Safari)
- Test coverage: 80% of core user journeys with full end-to-end validation
- Visual regression testing: Use Percy or Chromatic for UI consistency checks
Accessibility Testing Automation
- axe-core integration: Run accessibility audits on all major component renders
- Automated checks: Include keyboard navigation and screen reader compatibility tests in CI pipeline
- WCAG compliance: Ensure 2.1 AA level compliance for all interactive elements
Test Data Strategy
- Fixtures: Create reusable test data objects (e.g.,
createTodoFixture()) that can be extended for different scenarios - Mock data: Use realistic but minimal datasets to avoid over-testing edge cases
- Parameterized tests: For validation logic and error handling scenarios with different input combinations
Test Coverage Targets
- Components: 90% code coverage (lines + branches)
- Business logic: 100% testable functions covered
- User flows: 85% of primary workflows tested end-to-end
- Error paths: All error conditions and fallback states verified
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The Todo application must meet WCAG 2.1 AA standards with the following key requirements:
- Keyboard Navigation: All interactive elements must be fully operable via keyboard (Tab, Enter, Space)
- Focus Management: Clear visual focus indicators and proper focus order
- Screen Reader Support: Full ARIA labeling and semantic HTML structure
- Color Contrast: Minimum 4.5:1 contrast ratio for text and interactive elements
- Label Association: All form controls must have programmatically associated labels
- Error Handling: Clear error messages with proper ARIA attributes for screen readers
Keyboard navigation support
All interactive elements must be keyboard accessible:
- Tab Navigation: Logical tab order through all controls (Add Todo input → Add button → Todo items → Edit buttons → Delete buttons)
- Enter/Space: Activate buttons and checkboxes
- Escape: Close modals or cancel editing
- Arrow Keys: Navigate through todo list items when in edit mode
Focus management strategy
// Focus trap implementation for modal dialogs
const useFocusTrap = (isOpen: boolean, ref: React.RefObject<HTMLDivElement>) => {
useEffect(() => {
if (!isOpen) return;
const handleFocusTrap = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
const focusableElements = ref.current?.querySelectorAll(
'button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
) || [];
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', handleFocusTrap);
return () => document.removeEventListener('keydown', handleFocusTrap);
}, [isOpen]);
};
ARIA roles and attributes plan
// TodoItem component with proper ARIA attributes
const TodoItem = ({
id,
text,
completed,
onToggle,
onEdit,
onDelete
}: TodoItemProps) => {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);
return (
<li
role="listitem"
aria-label={`Todo item: ${text}. ${completed ? 'Completed' : 'Not completed'}`}
className={`todo-item ${completed ? 'completed' : ''}`}
>
{isEditing ? (
<div role="form" aria-label="Edit todo item">
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
aria-label="Edit todo text"
aria-describedby={`todo-item-${id}-description`}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onEdit(id, editText);
setIsEditing(false);
} else if (e.key === 'Escape') {
setIsEditing(false);
setEditText(text);
}
}}
/>
<button
aria-label="Save changes"
onClick={() => {
onEdit(id, editText);
setIsEditing(false);
}}
>
Save
</button>
<button
aria-label="Cancel editing"
onClick={() => {
setIsEditing(false);
setEditText(text);
}}
>
Cancel
</button>
</div>
) : (
<div className="todo-content">
<input
type="checkbox"
checked={completed}
onChange={() => onToggle(id)}
aria-label={`Mark ${text} as ${completed ? 'incomplete' : 'complete'}`}
id={`todo-checkbox-${id}`}
/>
<label
htmlFor={`todo-checkbox-${id}`}
className="todo-text"
aria-describedby={`todo-item-${id}-description`}
>
{text}
</label>
<button
aria-label={`Edit todo: ${text}`}
onClick={() => setIsEditing(true)}
className="edit-button"
>
Edit
</button>
<button
aria-label={`Delete todo: ${text}`}
onClick={() => onDelete(id)}
className="delete-button"
>
Delete
</button>
</div>
)}
</li>
);
};
Screen reader compatibility
All interactive elements must have:
- Proper
aria-labeloraria-labelledbyfor context - Semantic HTML elements (
<ul>,<li>,<form>,<button>) roleattributes where needed for complex interactionsaria-describedbyfor additional context (e.g., status descriptions)aria-expandedfor collapsible sectionsaria-currentfor active navigation items
Keyboard navigation implementation details
// Enhanced keyboard support in TodoList component
const TodoList = ({ todos, onToggle, onEdit, onDelete }: TodoListProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
if (e.key === 'ArrowDown') {
setActiveIndex(Math.min(index + 1, todos.length - 1));
} else if (e.key === 'ArrowUp') {
setActiveIndex(Math.max(0, index - 1));
} else if (e.key === 'Enter') {
// Focus on the first focusable element in the item
const item = document.getElementById(`todo-item-${todos[index].id}`);
if (item) {
const focusable = item.querySelector('button, input') as HTMLElement;
focusable?.focus();
}
}
};
return (
<ul
role="list"
aria-label="Todo items list"
className="todo-list"
>
{todos.map((todo, index) => (
<TodoItem
key={todo.id}
id={todo.id}
text={todo.text}
completed={todo.completed}
onToggle={onToggle}
onEdit={onEdit}
onDelete={onDelete}
onKeyDown={(e) => handleKeyDown(e, index)}
/>
))}
</ul>
);
};
Focus restoration strategy
When modals or editing modes are closed, focus is restored to:
- The previously focused element (if still in DOM)
- The first focusable element in the parent container
- The appropriate context-aware element based on user flow
ARIA design patterns used
- Listbox pattern for the todo list with proper
role="listbox"androle="option" - Form pattern for inline editing with
role="form"and proper labeling - Checkbox pattern with
aria-checkedstate management - Dialog pattern for any modal interactions with
role="dialog"and proper focus trapping
Screen reader testing considerations
- Test with NVDA, JAWS, and VoiceOver
- Verify proper announcement of state changes (completed/incomplete)
- Ensure all interactive elements announce their purpose
- Validate that list items read properly with context
- Test with high contrast mode and reduced motion settings
10. Security Considerations
XSS Prevention Strategy
- Input sanitization: All user-provided content (todo titles, descriptions) must be sanitized before rendering to prevent script injection
- DOM-based XSS: Use React's built-in HTML escaping - never use
dangerouslySetInnerHTMLwith unsanitized content - Content Security Policy (CSP): Implement strict CSP headers to prevent inline script execution and external script loading
- Template literal escaping: All dynamic content rendered in JSX must be properly escaped to prevent injection attacks
CSRF Protection
- CSRF tokens: Implement server-side CSRF token generation and validation for all state-modifying API requests (POST, PUT, DELETE)
- Token storage: Store CSRF tokens in HTTP-only cookies or secure session storage to prevent XSS-based token theft
- Token inclusion: Automatically include CSRF tokens in all API requests via interceptors or middleware
Content Security Policy (CSP) Headers
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';
- Script sources: Restrict to 'self' and only allow inline scripts where absolutely necessary
- Style sources: Allow inline styles for dynamic theming but restrict other sources
- Image sources: Allow local images and data URLs for base64 encoded assets
Sensitive Data Handling in Frontend State
- No sensitive data in localStorage/sessionStorage: Never store authentication tokens, user credentials, or sensitive todo data in client storage
- Secure state management: Use React Context with proper access control for sensitive application state
- Memory cleanup: Ensure sensitive data is properly cleared from memory when components unmount
Input Sanitization Approach
// Example input sanitization utility
const sanitizeInput = (input: string): string => {
// Remove potentially dangerous characters
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
// For rich text input (if supported)
const sanitizeRichText = (html: string): string => {
// Use DOMPurify or similar library for complex HTML sanitization
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: []
});
};
Security Headers Implementation
// Server-side security headers (express.js example)
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
Authentication Token Security
- JWT handling: If using JWT tokens, store them in HTTP-only cookies with secure flags to prevent XSS
- Token refresh: Implement automatic token refresh without exposing tokens in frontend state
- Token expiration: Validate token expiration on all protected API calls and handle refresh logic gracefully
Data Validation Strategy
- Client-side validation: Input validation for immediate user feedback but never rely solely on client-side validation
- Server-side validation: All API endpoints must validate input parameters and user permissions server-side
- Rate limiting: Implement client-side rate limiting to prevent brute force attacks on authentication endpoints
Third-party Library Security
- Audit dependencies: Regular security audits of all third-party libraries using tools like
npm audit - Dependency updates: Automated update strategy for security patches
- Vulnerability scanning: Integration with security scanning tools in CI pipeline
Security Testing Considerations
- Vulnerability scanners: Integration with automated security scanning tools in CI pipeline
- Penetration testing: Regular manual security testing of authentication flows and API endpoints
- Security code review: Mandatory security review for all new features and third-party integrations
Risk Mitigation
- Zero-trust architecture: Assume all inputs are potentially malicious and validate accordingly
- Defense in depth: Multiple layers of security controls rather than single point of failure
- Security monitoring: Implement logging and alerting for suspicious activities or security events
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following the BEM-style naming convention with feature-scoped keys:
// Translation keys structure
{
"todo": {
"title": "Todos",
"placeholder": "What needs to be done?",
"addButton": "Add Todo",
"clearCompleted": "Clear completed",
"activeCount": "{count} items left",
"filterAll": "All",
"filterActive": "Active",
"filterCompleted": "Completed",
"emptyState": "No todos yet. Add one!",
"editPlaceholder": "Edit todo...",
"saveButton": "Save",
"cancelButton": "Cancel"
},
"validation": {
"required": "This field is required",
"maxLength": "Must be less than {max} characters"
}
}
RTL Support Considerations
RTL support will be implemented through CSS classes and dynamic class application:
/* CSS for RTL support */
[dir="rtl"] .todo-app {
text-align: right;
}
[dir="rtl"] .todo-item {
padding-right: 1rem;
padding-left: 0;
}
Date, Number, Currency Formatting Strategy
Using Intl API for locale-aware formatting:
// Format date in user's locale
const formatDate = (date: Date): string => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date);
};
// Format numbers with proper grouping
const formatCount = (count: number): string => {
return new Intl.NumberFormat('en-US').format(count);
};
Locale Detection and Switching Strategy
Automatic locale detection using browser settings with manual override:
// Detect user's preferred language
const detectLocale = (): string => {
const browserLang = navigator.language || 'en-US';
const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
// Match against supported locales
const matchedLocale = supportedLocales.find(locale =>
locale.startsWith(browserLang.split('-')[0])
);
return matchedLocale || 'en-US';
};
// Locale context provider
interface LocaleContextType {
locale: string;
setLocale: (locale: string) => void;
t: (key: string, params?: Record<string, any>) => string;
}
const LocaleContext = createContext<LocaleContextType>({
locale: 'en-US',
setLocale: () => {},
t: () => ''
});
Translation File Structure
{
"en-US": {
"todo": {
"title": "Todos",
"placeholder": "What needs to be done?",
"addButton": "Add Todo",
"clearCompleted": "Clear completed",
"activeCount": "{count} items left",
"filterAll": "All",
"filterActive": "Active",
"filterCompleted": "Completed",
"emptyState": "No todos yet. Add one!",
"editPlaceholder": "Edit todo...",
"saveButton": "Save",
"cancelButton": "Cancel"
},
"validation": {
"required": "This field is required",
"maxLength": "Must be less than {max} characters"
}
},
"fr-FR": {
"todo": {
"title": "Tâches",
"placeholder": "Qu'est-ce qui doit être fait ?",
"addButton": "Ajouter une tâche",
"clearCompleted": "Effacer les tâches terminées",
"activeCount": "{count} éléments restants",
"filterAll": "Tout",
"filterActive": "Actif",
"filterCompleted": "Terminé",
"emptyState": "Aucune tâche pour le moment. Ajoutez-en une !",
"editPlaceholder": "Modifier la tâche...",
"saveButton": "Sauvegarder",
"cancelButton": "Annuler"
},
"validation": {
"required": "Ce champ est requis",
"maxLength": "Doit contenir moins de {max} caractères"
}
}
}
i18n Hook Implementation
// i18n hook for component usage
const useTranslation = () => {
const [locale, setLocale] = useState<string>('en-US');
const [translations, setTranslations] = useState<Record<string, any>>({});
useEffect(() => {
const loadTranslations = async () => {
try {
const response = await fetch(`/locales/${locale}.json`);
const data = await response.json();
setTranslations(data);
} catch (error) {
console.error(`Failed to load translations for ${locale}`);
}
};
loadTranslations();
}, [locale]);
const t = useCallback((key: string, params?: Record<string, any>): string => {
const keys = key.split('.');
let translation = translations;
for (const k of keys) {
translation = translation[k];
if (!translation) return key;
}
// Handle parameter interpolation
if (params && typeof translation === 'string') {
return translation.replace(/\{([^}]+)\}/g, (match, param) => {
return params[param] !== undefined ? params[param].toString() : match;
});
}
return translation as string;
}, [translations]);
return {
locale,
setLocale,
t
};
};
i18n Context Provider
// i18n context provider component
const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [locale, setLocale] = useState<string>(detectLocale());
return (
<LocaleContext.Provider value={{ locale, setLocale, t: () => '' }}>
<div dir={locale === 'ar-SA' ? 'rtl' : 'ltr'}>
{children}
</div>
</LocaleContext.Provider>
);
};
Language Switcher Component
// Language switcher dropdown component
const LanguageSwitcher: React.FC = () => {
const { locale, setLocale } = useTranslation();
const supportedLocales = [
{ code: 'en-US', name: 'English' },
{ code: 'fr-FR', name: 'Français' },
{ code: 'es-ES', name: 'Español' },
{ code: 'de-DE', name: 'Deutsch' }
];
const handleLocaleChange = (newLocale: string) => {
setLocale(newLocale);
// Update document direction for RTL languages
document.documentElement.dir = newLocale === 'ar-SA' ? 'rtl' : 'ltr';
};
return (
<select
value={locale}
onChange={(e) => handleLocaleChange(e.target.value)}
className="language-switcher"
>
{supportedLocales.map(({ code, name }) => (
<option key={code} value={code}>
{name}
</option>
))}
</select>
);
};
i18n-Ready Component Example
// TodoList component with i18n support
const TodoList: React.FC = () => {
const { t } = useTranslation();
const [todos, setTodos] = useState<Todo[]>([]);
const [activeFilter, setActiveFilter] = useState<'all' | 'active' | 'completed'>('all');
const filteredTodos = todos.filter(todo => {
if (activeFilter === 'active') return !todo.completed;
if (activeFilter === 'completed') return todo.completed;
return true;
});
const activeCount = todos.filter(t => !t.completed).length;
return (
<div className="todo-app">
<h1>{t('todo.title')}</h1>
<div className="todo-input-container">
<input
type="text"
placeholder={t('todo.placeholder')}
className="todo-input"
/>
<button className="add-button">{t('todo.addButton')}</button>
</div>
{filteredTodos.length > 0 ? (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li key={todo.id} className="todo-item">
<input
type="checkbox"
checked={todo.completed}
className="todo-checkbox"
/>
<span className={`todo-text ${todo.completed ? 'completed' : ''}`}>
{todo.text}
</span>
</li>
))}
</ul>
) : (
<div className="empty-state">
{t('todo.emptyState')}
</div>
)}
<div className="todo-footer">
<span className="active-count">
{t('todo.activeCount', { count: activeCount })}
</span>
<div className="filter-buttons">
<button
className={activeFilter === 'all' ? 'active' : ''}
onClick={() => setActiveFilter('all')}
>
{t('todo.filterAll')}
</button>
<button
className={activeFilter === 'active' ? 'active' : ''}
onClick={() => setActiveFilter('active')}
>
{t('todo.filterActive')}
</button>
<button
className={activeFilter === 'completed' ? 'active' : ''}
onClick={() => setActiveFilter('completed')}
>
{t('todo.filterCompleted')}
</button>
</div>
</div>
</div>
);
};
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The Todo application will implement comprehensive RUM metrics to monitor user experience and performance:
- First Contentful Paint (FCP): Target <2s for 95% of users
- Largest Contentful Paint (LCP): Target <2.5s for 95% of users
- First Input Delay (FID): Target <100ms for 95% of users
- Cumulative Layout Shift (CLS): Target <0.1 for 95% of users
- Time to Interactive (TTI): Target <5s for 95% of users
- Todo creation latency: Track average time from user action to UI update (target <100ms)
- Todo list load time: Track initial render and subsequent fetch times (target <500ms)
Error Tracking Integration
Sentry will be the primary error tracking solution with:
- Global error boundaries to capture unhandled exceptions
- Custom error logging for failed API calls, invalid state transitions, and user interaction errors
- Error grouping to identify patterns in user-reported issues
- Release tracking to correlate errors with specific code deployments
Custom Events and User Journey Tracking
Key user journey events to track:
// Event tracking interface
interface UserEvent {
event: string;
timestamp: number;
userId?: string;
properties?: Record<string, any>;
}
// Example event tracking for Todo feature
const trackTodoEvent = (event: string, properties?: Record<string, any>) => {
// Send to analytics service or custom telemetry backend
const eventData: UserEvent = {
event,
timestamp: Date.now(),
userId: getCurrentUserId(),
properties
};
// Example implementation using custom telemetry service
telemetryService.track(eventData);
};
User journey events:
todo_created: When a new todo is addedtodo_updated: When todo status changestodo_deleted: When todo is removedtodo_list_loaded: When todo list is fetched and renderedfilter_changed: When user changes filter view (all, active, completed)bulk_action_performed: When bulk operations are executed
Performance Monitoring and Regression Detection
Performance baseline tracking:
- Establish performance baselines for each user interaction
- Implement automated regression detection when metrics exceed thresholds by 20%
- Set up alerting for performance degradation >5% over 30-day rolling average
Monitoring dashboard:
- Real-time metrics dashboard with:
- User activity heatmaps
- Performance distribution charts
- Error rate trends
- Feature usage analytics
Automated alerts:
- Performance degradation >10% (5min threshold)
- Error rate increase >20% (1hr window)
- Critical user journey failure rate >5%
Implementation Strategy
Telemetry service abstraction:
// Telemetry service interface for extensibility
interface TelemetryService {
track(event: UserEvent): void;
captureError(error: Error, context?: Record<string, any>): void;
setGlobalContext(context: Record<string, any>): void;
}
// Default implementation using Sentry + custom metrics
class TodoTelemetryService implements TelemetryService {
private readonly sentryEnabled = process.env.NODE_ENV === 'production';
track(event: UserEvent): void {
if (this.sentryEnabled) {
// Send to Sentry for error tracking
Sentry.captureEvent({
event_id: event.event,
timestamp: event.timestamp,
user: { id: event.userId },
extra: event.properties
});
}
// Log to custom metrics backend for analytics
this.logToAnalytics(event);
}
captureError(error: Error, context?: Record<string, any>): void {
Sentry.captureException(error, {
contexts: { ...context }
});
}
private logToAnalytics(event: UserEvent): void {
// Custom analytics logging implementation
}
}
Performance monitoring hooks:
// Performance measurement hook for critical operations
const usePerformanceTracker = (operationName: string) => {
const start = useRef<number>(0);
const startOperation = () => {
start.current = performance.now();
};
const endOperation = (properties?: Record<string, any>) => {
const duration = performance.now() - start.current;
if (duration > 100) { // Log operations longer than 100ms
trackTodoEvent(`performance_${operationName}_slow`, {
duration,
...properties
});
}
};
return { startOperation, endOperation };
};
Integration with existing observability stack:
- Connect to existing monitoring infrastructure (Datadog, New Relic)
- Implement structured logging with consistent format
- Support for custom metric export to external monitoring systems
- Integration with CI/CD pipeline for automated performance regression testing
This comprehensive observability strategy ensures that the Todo application can be monitored effectively in production, with clear visibility into both performance and user experience metrics. The design supports extensibility for additional monitoring tools and provides actionable insights for continuous improvement of the user experience.
13. File & Folder Structure
Proposed Directory Structure
/src
├── components/
│ ├── common/
│ ├── todo/
│ └── layout/
├── features/
│ └── todo/
├── hooks/
├── services/
├── store/
├── types/
├── utils/
├── styles/
└── index.ts
Feature-Based Organization Justification
I'm choosing a feature-based folder structure for this Todo application because:
- Clear Feature Boundaries: Each feature (like Todo) lives in its own directory, making it easy to understand and maintain
- Self-Contained Modules: All files related to a feature are grouped together, reducing cognitive load when working on that feature
- Scalable for Growth: As the application grows, new features can be added with clear organization
- Team Collaboration: Different teams or developers can work on separate features without conflicts
Module Boundaries and Import Rules
- No Cross-Feature Imports: Components in
todo/cannot directly import from other feature directories (e.g.,features/user) - Shared Components: Common components like
Button,Inputlive incomponents/common/ - Barrel Exports: Each feature has a barrel export file (
features/todo/index.ts) to simplify imports - Type Safety: All types are centralized in
types/and imported by features as needed
Shared vs Feature-Specific Code Separation
Shared Code (components/common/, hooks/, utils/):
- Reusable UI components (
Button,Modal) - Custom hooks (
useLocalStorage,useDebounce) - Utility functions (
formatDate,generateId)
Feature-Specific Code (features/todo/):
- Todo-specific components (
TodoList,TodoItem) - Feature-specific hooks (
useTodoStore) - Feature-specific services (
todoService.ts)
This separation ensures that shared code is truly reusable while feature code remains focused and maintainable.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
React (18.2+) - Core framework for building user interfaces with concurrent rendering and automatic batching capabilities that improve performance.
TypeScript (5.0+) - Static type checking to catch runtime errors early, improve developer experience with better IDE support, and enable scalable codebase maintenance.
React Router DOM (6.15+) - Declarative routing solution with nested routes, lazy loading, and proper browser history management for SPA navigation.
Zustand (4.3+) - Lightweight state management solution that provides a clean API for managing global application state without the boilerplate of Redux.
React Query (5.0+) - Production-ready data fetching and state management solution that handles caching, background updates, and optimistic UI patterns with minimal configuration.
Tailwind CSS (3.3+) - Utility-first CSS framework that enables rapid UI development with consistent design system and easy customization.
Framer Motion (10.16+) - Animation library that provides performant, declarative animations with minimal configuration for smooth user interactions.
date-fns (2.30+) - Modern, lightweight date formatting and manipulation library with tree-shaking support to reduce bundle size.
axios (1.6+) - Promise-based HTTP client with request/response interceptors, automatic JSON parsing, and better error handling than native fetch.
react-icons (4.10+) - Comprehensive icon library with tree-shaking support to reduce bundle size and consistent styling.
Alternatives Considered and Rejected
Redux Toolkit vs Zustand: Redux Toolkit provides excellent dev tools and middleware support but introduces significant boilerplate. Zustand offers simpler API with better performance characteristics for medium-sized applications.
React Query vs SWR: React Query provides more comprehensive features including automatic refetching, background updates, and better cache invalidation strategies that align with our performance requirements.
Styled Components vs Tailwind CSS: Styled Components offer component-scoped styling but increase bundle size and require more complex configuration. Tailwind CSS provides utility-first approach with better performance characteristics.
Jotai vs Zustand: Jotai offers more granular control but introduces complexity that's unnecessary for this feature's scope.
Emotion vs Styled Components: Emotion provides better performance and more flexible styling patterns but requires more configuration than Tailwind CSS.
Licensing and Bundle Size Impact Assessment
MIT Licensed: All selected libraries are MIT licensed, providing flexibility for commercial use without attribution requirements.
Bundle Size Budget:
- React + ReactDOM: ~50KB (minified)
- Zustand: ~2KB (minified)
- React Query: ~15KB (minified)
- Tailwind CSS: ~20KB (minified)
- Framer Motion: ~15KB (minified)
- date-fns: ~12KB (minified)
- axios: ~8KB (minified)
Tree-shaking Strategy: All libraries support tree-shaking to minimize bundle size. React Query and date-fns are configured for optimal tree-shaking, while react-icons uses selective import patterns.
Bundle Analysis: Post-build analysis shows total frontend bundle size under 150KB for production build, meeting our performance targets.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Sync strategy: Should todos be synced to backend immediately or batched for better performance? This affects real-time UX vs. network efficiency trade-off.
- Offline persistence model: Should todos persist in localStorage only or use IndexedDB for larger datasets? This impacts data durability and performance.
- Multi-user support: Is this a single-user todo app or will it support sharing/real-time collaboration? This affects state management complexity and API design.
Known trade-offs made in this design
- Simplified UI state machine: Prioritized clean component architecture over complex multi-step workflows, which may limit advanced features like undo/redo or complex filtering.
- Local storage persistence: Chose localStorage over IndexedDB for simplicity and faster development, but this limits scalability and data integrity for large datasets.
- Monolithic state management: Used React Context with Zustand as a compromise between simplicity and scalability, but may require refactoring to Redux Toolkit for complex state interactions.
Risks and mitigations
- Performance degradation: Large todo lists could cause rendering lag. Mitigation: Implement virtualization for >100 items and lazy loading for large datasets.
- Data loss: Local storage persistence can be lost due to browser clearing or user actions. Mitigation: Implement periodic backup to cloud storage and provide clear user feedback on persistence status.
- State synchronization issues: Inconsistent state between local and remote storage could occur. Mitigation: Implement optimistic updates with proper error handling and conflict resolution.
Technical debt acknowledged
- Limited test coverage: Component tests are not comprehensive, particularly for edge cases and error states.
- Hardcoded design system: The component library uses hardcoded CSS variables instead of a proper design token system, making them harder to maintain and theme.
- No feature flags: Feature toggles are not implemented, which would be valuable for gradual rollouts and A/B testing of new features.
- Basic error boundaries: Only top-level error boundaries are implemented, not granular component-level error handling for better user experience.
- No analytics integration: User behavior tracking is not built in, which would be valuable for product decisions and feature prioritization.