Skip to main content

News Feed UI Architecture Design

Comprehensive low-level design for scalable news feed UI with real-time updates, infinite scrolling, and content personalization

This document presents a scalable architecture for news feed UI systems, covering component structure, data flow patterns, and performance optimizations essential for modern social media and content platforms.

1. Problem Statement & Requirements

Clarifying questions

  1. What target devices/browsers should this news feed support? (e.g., mobile-first, desktop, tablet, IE11, Safari 15+)
  2. Do we need to support real-time updates or can we rely on periodic polling for content freshness?
  3. Should this feed work offline with cached content, or is it strictly online-first?
  4. What level of accessibility compliance is required? (e.g., WCAG 2.1 AA, Section 508)
  5. Do we need i18n support for multiple languages or is English-only sufficient?
  6. Are there any design system constraints we must follow (e.g., existing component library, design tokens)?
  7. What's the expected scale of content (e.g., 100s vs 1000s of articles, daily article count)?

Functional requirements

  • Display a chronological feed of news articles from various sources
  • Support article filtering by category, source, or keyword search
  • Allow users to like, bookmark, or share articles
  • Show article metadata (author, publication time, category)
  • Provide infinite scroll or pagination for content loading
  • Support dark/light mode toggle
  • Display article thumbnails and summaries
  • Show loading states and empty states for content

Non-functional requirements

  • Performance: LCP < 2.5s, CLS < 0.1, INP < 200ms
  • Accessibility: WCAG 2.1 AA compliant with keyboard navigation and ARIA support
  • Scalability: Support 10k+ concurrent users with 500+ articles per feed
  • Reliability: Graceful degradation when API calls fail or content is unavailable

Constraints and assumptions

  • Assumption: News articles are provided via a third-party API (not built-in)
  • Constraint: Must support mobile-first responsive design (smallest 320px width)
  • Assumption: Users have basic internet connectivity (4G+)
  • Constraint: Content must be displayed within 2 seconds of page load
  • Assumption: No server-side rendering needed for this feature

Out of scope

  • User authentication and authorization (login/logout flows)
  • Content creation or editing capabilities
  • Advanced analytics or user behavior tracking
  • Push notifications for breaking news
  • Comments or discussion threads on articles
  • Article content editing or formatting (HTML/CSS rendering)
  • Third-party ad integration or monetization
  • Social media sharing beyond basic share buttons

2. Component Architecture

graph TD
    A[NewsFeed] --> B[NewsCard]
    A --> C[NewsList]
    A --> D[NewsFilters]
    A --> E[LoadingSkeleton]
    A --> F[EmptyState]
    A --> G[ErrorBoundary]
    C --> H[NewsCard]
    C --> I[LoadMoreButton]
    B --> J[NewsImage]
    B --> K[NewsTitle]
    B --> L[NewsSummary]
    B --> M[NewsMeta]
    D --> N[CategoryFilter]
    D --> O[SearchInput]
    D --> P[SortBySelect]

Component Tree and Breakdown

The News Feed UI follows a hierarchical component structure with clear separation of concerns:

Smart Components (Container Components):

  • NewsFeed - Main container that orchestrates data fetching, filtering, and state management
  • NewsList - Handles list rendering with virtualization and pagination logic
  • NewsFilters - Manages filter state and UI interactions

Dumb Components (Presentational Components):

  • NewsCard - Renders individual news item with image, title, summary, and metadata
  • NewsImage - Displays news thumbnails with lazy loading support
  • NewsTitle - Renders headline text with proper typography
  • NewsSummary - Truncates and displays article summaries
  • NewsMeta - Displays publication date, category, and author information
  • CategoryFilter - Renders filter options for news categories
  • SearchInput - Handles search term input with debouncing
  • SortBySelect - Dropdown for sorting options
  • LoadMoreButton - Infinite scroll trigger component
  • LoadingSkeleton - Skeleton loading state for better perceived performance
  • EmptyState - Displays when no news items are available

TypeScript Props Contracts

// NewsCard.tsx
interface NewsCardProps {
  article: Article;
  onReadMore?: () => void;
  onClick?: () => void;
}

// NewsFeed.tsx
interface NewsFeedProps {
  initialArticles?: Article[];
  categories?: string[];
  defaultSort?: 'newest' | 'oldest' | 'popular';
  searchQuery?: string;
  onArticleClick?: (article: Article) => void;
}

// NewsFilters.tsx
interface NewsFiltersProps {
  categories: string[];
  selectedCategory: string | null;
  searchQuery: string;
  sortOption: 'newest' | 'oldest' | 'popular';
  onCategoryChange: (category: string) => void;
  onSearchChange: (query: string) => void;
  onSortChange: (sort: 'newest' | 'oldest' | 'popular') => void;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Loading : fetchArticles()
    Loading --> Success : articlesLoaded
    Loading --> Error : fetchFailed
    Success --> Loading : loadMore()
    Success --> Idle : resetFilters()
    Error --> Idle : retryFetch()
    Error --> Loading : retryFetch()

Component Responsibilities

  • NewsFeed: Main orchestrator that manages the entire news feed lifecycle, including data fetching, filtering, and state management
  • NewsList: Renders the list of articles with virtualization for performance, handles infinite scroll, and manages loading states
  • NewsCard: Individual article presentation with proper accessibility attributes and click handlers
  • NewsFilters: Manages filter state and UI interactions for category, search, and sorting
  • LoadingSkeleton: Provides better perceived performance during data loading
  • EmptyState: Displays appropriate messaging when no articles match current filters

Reusability Decisions

  • NewsCard is designed to be reusable across different news contexts (featured, trending, search results)
  • NewsFilters can be extracted for use in other content filtering scenarios
  • LoadingSkeleton is generic and can be reused in any data-loading context
  • NewsImage supports various image formats and has fallback handling for broken images

Component Lifecycle Considerations

  • NewsFeed mounts with initial data fetch and sets up infinite scroll listeners
  • NewsList handles virtualization efficiently during scroll events
  • NewsCard implements proper image loading lifecycle with lazy loading and error handling
  • NewsFilters debounces search input to prevent excessive API calls
  • All components implement proper cleanup for event listeners and subscriptions to prevent memory leaks

State Management Integration Points

The component architecture is designed to integrate seamlessly with the state management system:

  • NewsFeed consumes global article store state
  • NewsFilters manages local filter state while syncing with global filters
  • NewsList handles pagination and loading states internally
  • All components properly handle loading, success, and error states through their props contracts

3. State Management Design

Local state vs. global state decisions

For a news feed UI, we need to carefully distinguish between local and global state:

  • Local state: Component-specific data like active tab, search query, or loading indicators within individual feed items
  • Global state: Data that needs to be shared across the entire application like user preferences, authentication status, or the complete news feed content

State management approach

I'll use Zustand as the primary state management solution with the following rationale:

  • Lightweight: No boilerplate, simple API that feels like React's useState
  • DevTools support: Excellent integration with React DevTools and Redux DevTools
  • TypeScript friendly: First-class TypeScript support with minimal boilerplate
  • Scalable: Can easily migrate to Redux Toolkit if needed later

Zustand is ideal for this use case because:

  1. News feed data is primarily read-heavy with occasional mutations
  2. The state shape is manageable and doesn't require complex middleware
  3. It's simpler to reason about than Redux Toolkit for this scope

State shape and normalization strategy

// Normalized state structure to prevent duplication and enable efficient updates
interface NewsItem {
  id: string;
  title: string;
  content: string;
  author: string;
  publishedAt: Date;
  source: string;
  category: string;
  imageUrl?: string;
  readTime?: number;
  isBookmarked?: boolean;
  isRead?: boolean;
}

interface NewsFeedState {
  // Normalized data structure
  articles: Record<string, NewsItem>;
  articleIds: string[];
  
  // UI state
  loading: boolean;
  error: string | null;
  currentPage: number;
  totalPages: number;
  
  // User preferences
  categories: string[];
  bookmarkedArticles: string[];
  readArticles: string[];
  
  // Filtering and search
  activeCategory: string | null;
  searchQuery: string;
  
  // Pagination and infinite scroll
  hasMore: boolean;
  isLoadingMore: boolean;
}

Server state vs. client state separation

  • Server state: News articles, user preferences, bookmarks from backend
  • Client state: UI states (loading, error, search), local preferences, read status tracking

Cache invalidation strategy

  • Time-based: Articles older than 24 hours are invalidated
  • User action: When user bookmarks/unbookmarks, cache is updated immediately
  • Manual refresh: User can trigger refresh to invalidate all cached data

Optimistic updates design

For bookmark operations, we implement optimistic updates:

  1. Immediately update local state to show article as bookmarked
  2. Send request to backend
  3. If successful, keep local update
  4. If failed, revert local state and show error

Critical-path hook implementation

// src/hooks/useNewsFeed.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface NewsItem {
  id: string;
  title: string;
  content: string;
  author: string;
  publishedAt: Date;
  source: string;
  category: string;
  imageUrl?: string;
  readTime?: number;
  isBookmarked?: boolean;
  isRead?: boolean;
}

interface NewsFeedState {
  articles: Record<string, NewsItem>;
  articleIds: string[];
  loading: boolean;
  error: string | null;
  currentPage: number;
  totalPages: number;
  categories: string[];
  bookmarkedArticles: string[];
  readArticles: string[];
  activeCategory: string | null;
  searchQuery: string;
  hasMore: boolean;
  isLoadingMore: boolean;
}

interface NewsFeedActions {
  fetchArticles: (page?: number, category?: string) => Promise<void>;
  loadMoreArticles: () => Promise<void>;
  searchArticles: (query: string) => void;
  setCategory: (category: string | null) => void;
  toggleBookmark: (articleId: string) => Promise<void>;
  markAsRead: (articleId: string) => void;
  setError: (error: string | null) => void;
  setLoading: (loading: boolean) => void;
}

const useNewsFeedStore = create<NewsFeedState & NewsFeedActions>(
  persist(
    (set, get) => ({
      // Initial state
      articles: {},
      articleIds: [],
      loading: false,
      error: null,
      currentPage: 1,
      totalPages: 1,
      categories: ['Technology', 'Politics', 'Sports', 'Entertainment'],
      bookmarkedArticles: [],
      readArticles: [],
      activeCategory: null,
      searchQuery: '',
      hasMore: true,
      isLoadingMore: false,

      // Actions
      fetchArticles: async (page = 1, category = null) => {
        set({ loading: true, error: null });
        
        try {
          // Simulate API call
          const response = await fetch(
            `/api/news?page=${page}&category=${category || ''}&limit=10`
          );
          
          if (!response.ok) {
            throw new Error('Failed to fetch articles');
          }
          
          const data = await response.json();
          
          set((state) => {
            const newArticles = data.articles.reduce((acc: Record<string, NewsItem>, article: any) => {
              acc[article.id] = {
                ...article,
                publishedAt: new Date(article.publishedAt),
                isBookmarked: state.bookmarkedArticles.includes(article.id),
                isRead: state.readArticles.includes(article.id)
              };
              return acc;
            }, {});
            
            const newArticleIds = data.articles.map((a: any) => a.id);
            
            return {
              articles: { ...state.articles, ...newArticles },
              articleIds: newArticleIds,
              currentPage: page,
              totalPages: data.totalPages,
              hasMore: page < data.totalPages,
              loading: false
            };
          });
        } catch (error) {
          set({ 
            error: error instanceof Error ? error.message : 'Unknown error',
            loading: false 
          });
        }
      },

      loadMoreArticles: async () => {
        const { currentPage, hasMore, isLoadingMore } = get();
        
        if (!hasMore || isLoadingMore) return;
        
        set({ isLoadingMore: true });
        
        try {
          await get().fetchArticles(currentPage + 1);
        } finally {
          set({ isLoadingMore: false });
        }
      },

      searchArticles: (query: string) => {
        set({ searchQuery: query });
      },

      setCategory: (category: string | null) => {
        set({ 
          activeCategory: category,
          currentPage: 1,
          articleIds: [],
          articles: {}
        });
        
        // Fetch articles for new category
        get().fetchArticles(1, category);
      },

      toggleBookmark: async (articleId: string) => {
        const { bookmarkedArticles, articles } = get();
        const isBookmarked = bookmarkedArticles.includes(articleId);
        
        // Optimistic update
        set((state) => ({
          bookmarkedArticles: isBookmarked 
            ? state.bookmarkedArticles.filter(id => id !== articleId)
            : [...state.bookmarkedArticles, articleId],
          articles: {
            ...state.articles,
            [articleId]: {
              ...state.articles[articleId],
              isBookmarked: !isBookmarked
            }
          }
        }));
        
        try {
          await fetch(`/api/articles/${articleId}/bookmark`, {
            method: isBookmarked ? 'DELETE' : 'POST'
          });
        } catch (error) {
          // Revert optimistic update on failure
          set((state) => ({
            bookmarkedArticles: isBookmarked 
              ? [...state.bookmarkedArticles, articleId]
              : state.bookmarkedArticles.filter(id => id !== articleId),
            articles: {
              ...state.articles,
              [articleId]: {
                ...state.articles[articleId],
                isBookmarked: !isBookmarked
              }
            }
          }));
          
          throw new Error('Failed to update bookmark');
        }
      },

      markAsRead: (articleId: string) => {
        set((state) => ({
          readArticles: [...state.readArticles, articleId],
          articles: {
            ...state.articles,
            [articleId]: {
              ...state.articles[articleId],
              isRead: true
            }
          }
        }));
      },

      setError: (error: string | null) => {
        set({ error });
      },

      setLoading: (loading: boolean) => {
        set({ loading });
      }
    }),
    {
      name: 'news-feed-storage',
      partialize: (state) => ({
        bookmarkedArticles: state.bookmarkedArticles,
        readArticles: state.readArticles,
        categories: state.categories
      })
    }
  )
);

// Custom hook for news feed operations
export const useNewsFeed = () => {
  const {
    articles,
    articleIds,
    loading,
    error,
    currentPage,
    totalPages,
    categories,
    bookmarkedArticles,
    readArticles,
    activeCategory,
    searchQuery,
    hasMore,
    isLoadingMore,
    fetchArticles,
    loadMoreArticles,
    searchArticles,
    setCategory,
    toggleBookmark,
    markAsRead,
    setError,
    setLoading
  } = useNewsFeedStore();

  return {
    // State
    articles,
    articleIds,
    loading,
    error,
    currentPage,
    totalPages,
    categories,
    bookmarkedArticles,
    readArticles,
    activeCategory,
    searchQuery,
    hasMore,
    isLoadingMore,

    // Actions
    fetchArticles,
    loadMoreArticles,
    searchArticles,
    setCategory,
    toggleBookmark,
    markAsRead,
    setError,
    setLoading
  };
};

// Selector hook for filtered articles
export const useFilteredArticles = () => {
  const { articles, articleIds, activeCategory, searchQuery } = useNewsFeed();
  
  return useMemo(() => {
    let filteredIds = articleIds;
    
    // Apply category filter
    if (activeCategory) {
      filteredIds = filteredIds.filter(id => 
        articles[id]?.category === activeCategory
      );
    }
    
    // Apply search filter
    if (searchQuery) {
      const query = searchQuery.toLowerCase();
      filteredIds = filteredIds.filter(id => 
        articles[id]?.title.toLowerCase().includes(query) ||
        articles[id]?.content.toLowerCase().includes(query)
      );
    }
    
    return filteredIds.map(id => articles[id]);
  }, [articles, articleIds, activeCategory, searchQuery]);
};

The critical-path hook provides:

  1. Normalized state management for efficient updates and rendering
  2. Optimistic UI updates for better perceived performance
  3. Persistence using localStorage to maintain user preferences across sessions
  4. Type safety with full TypeScript typing throughout
  5. Comprehensive error handling with proper fallbacks and user feedback
  6. Scalable architecture that can grow with additional features

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[/forgot-password]
    C --> G[/feed]
    C --> H[/profile]
    C --> I[/settings]
    G --> J[/feed/latest]
    G --> K[/feed/trending]
    G --> L[/feed/saved]

The news feed application follows a protected route structure with the following hierarchy:

  • Public routes: /login, /register, /forgot-password - accessible without authentication
  • Protected routes: /feed, /profile, /settings - require valid session
  • Feed sub-routes: /feed/latest, /feed/trending, /feed/saved - content filtering

Code Splitting Strategy

Route-based code splitting with React.lazy and Suspense:

// routes/index.tsx
import { lazy, Suspense } from 'react';
import { Route, Routes, Navigate } from 'react-router-dom';

const Login = lazy(() => import('../features/auth/Login'));
const Register = lazy(() => import('../features/auth/Register'));
const ForgotPassword = lazy(() => import('../features/auth/ForgotPassword'));
const Feed = lazy(() => import('../features/feed/Feed'));
const Profile = lazy(() => import('../features/profile/Profile'));
const Settings = lazy(() => import('../features/settings/Settings'));

export const AppRoutes = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <Routes>
      <Route path="/login" element={<Login />} />
      <Route path="/register" element={<Register />} />
      <Route path="/forgot-password" element={<ForgotPassword />} />
      <Route element={<ProtectedRoute />}>
        <Route path="/feed" element={<Feed />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/settings" element={<Settings />} />
      </Route>
      <Route path="*" element={<Navigate to="/feed" replace />} />
    </Routes>
  </Suspense>
);

Justification:

  • Route-based splitting reduces initial bundle size and improves load times
  • Feed page contains the heaviest components (infinite scroll, article cards)
  • Auth pages are lightweight and can be loaded together
  • Protected route wrapper ensures consistent authentication checks
// components/ProtectedRoute.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth';

export const ProtectedRoute = ({ children }: { children?: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, isLoading } = useAuthStore();

  useEffect(() => {
    if (!isAuthenticated && !isLoading) {
      navigate('/login', { replace: true });
    }
  }, [isAuthenticated, isLoading, navigate]);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return isAuthenticated ? <>{children}</> : null;
};

Deep Linking Support and URL Parameter Design

// features/feed/types.ts
export interface FeedParams {
  category?: 'latest' | 'trending' | 'saved';
  page?: number;
  search?: string;
}

export interface Article {
  id: string;
  title: string;
  content: string;
  author: {
    name: string;
    avatarUrl: string;
  };
  publishedAt: Date;
  category: string;
  isSaved: boolean;
  readTime: number;
}

URL structure:

  • /feed/latest?page=1&search=tech - Latest articles with search filter
  • /feed/trending?category=technology&page=2 - Trending articles by category
  • /feed/saved?page=1 - Saved articles

Browser History Management and Scroll Restoration

// app/history.ts
import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

// app/ScrollToTop.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const ScrollToTop = () => {
  const location = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [location]);

  return null;
};

// app/index.tsx
import { ScrollToTop } from './components/ScrollToTop';

const App = () => (
  <>
    <ScrollToTop />
    <AppRoutes />
  </>
);

The scroll restoration is handled automatically by React Router v6's useScrollRestoration hook, but custom scroll behavior can be implemented for specific use cases like infinite scrolling where we want to maintain position when switching between feed categories.

The routing strategy ensures:

  1. Clean separation of public and protected content
  2. Efficient code loading with minimal initial payload
  3. Consistent authentication flow across the application
  4. Support for deep linking and search engine optimization
  5. Proper browser history management for user experience

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using a fetch wrapper with Axios as the underlying HTTP client. This approach provides better request/response interception, error handling, and consistent configuration across all API calls.

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

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com';

const apiClient: AxiosInstance = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor for adding auth token
apiClient.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 handling errors
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle token expiration
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Request/Response Interceptors

Request Interceptor:

  • Adds authentication token to all requests
  • Sets default headers for content type and cache control
  • Logs request metadata for debugging

Response Interceptor:

  • Handles 401 Unauthorized responses by redirecting to login
  • Normalizes error responses for consistent handling
  • Implements automatic retry logic for transient failures

Error Handling and Retry Strategy

// api/errorHandler.ts
import { AxiosError } from 'axios';

export class NewsFeedApiError extends Error {
  constructor(
    public status: number,
    public message: string,
    public originalError?: AxiosError
  ) {
    super(message);
    this.name = 'NewsFeedApiError';
  }
}

export const handleApiError = (error: AxiosError): NewsFeedApiError => {
  if (error.response) {
    return new NewsFeedApiError(
      error.response.status,
      error.response.data?.message || 'API Error occurred',
      error
    );
  }
  
  if (error.request) {
    return new NewsFeedApiError(
      503,
      'Network error - please check your connection',
      error
    );
  }
  
  return new NewsFeedApiError(
    500,
    'Unexpected error occurred',
    error
  );
};

// Retry logic with exponential backoff
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  maxRetries = 3,
  delay = 1000
): Promise<any> => {
  let lastError: Error;
  
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      
      if (i === maxRetries) break;
      
      // Exponential backoff
      await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
    }
  }
  
  throw lastError;
};

Loading, Error, and Empty States Design

The API integration layer will support three distinct states for each data fetch operation:

  1. Loading State: Shows skeleton loaders or progress indicators
  2. Error State: Displays user-friendly error messages with retry options
  3. Empty State: Shows appropriate message when no content is available

Data Fetching Strategy

For a news feed, we'll use Client-Side Rendering (CSR) with React Query for optimal data fetching and caching:

// hooks/useNewsFeed.ts
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import apiClient from '../api/client';

export interface NewsArticle {
  id: string;
  title: string;
  content: string;
  author: {
    name: string;
    avatarUrl?: string;
  };
  publishedAt: string;
  category: string;
  imageUrl?: string;
  readTime?: number;
}

export interface NewsFeedResponse {
  articles: NewsArticle[];
  total: number;
  page: number;
  limit: number;
}

const fetchNewsFeed = async (page: number, limit: number): Promise<NewsFeedResponse> => {
  const response = await apiClient.get<NewsFeedResponse>('/articles', {
    params: { page, limit }
  });
  
  return response.data;
};

export const useNewsFeed = (page: number = 1, limit: number = 10) => {
  return useQuery<NewsFeedResponse, NewsFeedApiError>({
    queryKey: ['news-feed', page, limit],
    queryFn: () => fetchNewsFeed(page, limit),
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
    retry: 2,
    retryDelay: 1000,
  });
};

// Infinite scroll implementation
export const useInfiniteNewsFeed = (limit: number = 10) => {
  return useInfiniteQuery<NewsFeedResponse, NewsFeedApiError>({
    queryKey: ['news-feed-infinite'],
    queryFn: ({ pageParam = 1 }) => fetchNewsFeed(pageParam, limit),
    getNextPageParam: (lastPage) => {
      const nextPage = Math.ceil(lastPage.total / limit);
      return lastPage.articles.length < limit ? undefined : nextPage;
    },
    staleTime: 5 * 60 * 1000,
  });
};

API Response Transformation Layer

// api/transformers.ts
import { NewsArticle } from '../types/news';

export const transformNewsArticle = (rawArticle: any): NewsArticle => ({
  id: rawArticle.id,
  title: rawArticle.title,
  content: rawArticle.content,
  author: {
    name: rawArticle.author.name,
    avatarUrl: rawArticle.author.avatarUrl,
  },
  publishedAt: new Date(rawArticle.publishedAt).toISOString(),
  category: rawArticle.category,
  imageUrl: rawArticle.imageUrl,
  readTime: rawArticle.readTime,
});

export const transformNewsFeedResponse = (rawResponse: any): NewsFeedResponse => ({
  articles: rawResponse.articles.map(transformNewsArticle),
  total: rawResponse.total,
  page: rawResponse.page,
  limit: rawResponse.limit,
});

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant C as NewsFeed Component
    participant H as useNewsFeed Hook
    participant Q as React Query
    participant A as API Client
    participant S as News Service

    U->>C: Scroll to bottom or refresh
    C->>H: Call useNewsFeed()
    H->>Q: Query cache for articles
    Q->>H: Return cached data if fresh
    H->>A: Fetch new articles from API
    A->>S: Request articles with pagination
    S-->>A: Return JSON response
    A-->>H: Return transformed data
    H-->>C: Update component with new articles
    C->>U: Render updated news feed

The API integration layer is designed to handle the typical challenges of a news feed: efficient data fetching, proper error handling, caching strategies for performance optimization, and support for infinite scrolling. The use of React Query provides built-in caching, automatic refetching, and optimistic updates capabilities that are essential for a responsive news feed experience.

6. Performance Design

Core Web Vitals targets (LCP, INP, CLS) with specific numbers

  • Largest Contentful Paint (LCP): Target 2.5s or less for content to become visible
  • Interaction to Next Paint (INP): Target 200ms or less for user interactions to respond
  • Cumulative Layout Shift (CLS): Target 0.1 or less for layout stability

Bundle size budget per route

  • News Feed Route: 150KB (minified + gzipped) including all dependencies and assets
  • Article Detail Route: 100KB (minified + gzipped) for detailed content view
  • User Profile Route: 50KB (minified + gzipped) for user-centric views

Critical rendering path optimization

  • Critical CSS: Inline critical CSS for news feed layout and loading states
  • Async JavaScript: Defer non-critical JS execution using defer attribute
  • Resource Hints: Preload key assets like fonts and critical images using <link rel="preload">
  • Code Splitting: Route-based splitting to load only necessary code for each view

Image optimization strategy (formats, lazy loading, responsive images)

// Image component with optimization
const OptimizedImage = ({ 
  src, 
  alt, 
  width, 
  height,
  priority = false
}: {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  priority?: boolean;
}) => {
  return (
    <img
      src={src}
      alt={alt}
      width={width}
      height={height}
      loading={priority ? "eager" : "lazy"}
      decoding="async"
      sizes="(max-width: 768px) 100vw, 50vw"
      srcSet={`
        ${src}?w=320 320w,
        ${src}?w=640 640w,
        ${src}?w=960 960w,
        ${src}?w=1280 1280w
      `}
    />
  );
};

Font loading strategy

  • Font Display: Use font-display: swap to prevent FOIT
  • Preload Critical Fonts: <link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
  • Font Subsets: Load only necessary character subsets for each language
  • Fallback Fonts: Define appropriate fallback fonts in CSS

Memoization strategy (useMemo, useCallback, React.memo) — what to memoize and what not to

// Memoized components for performance
const NewsCard = React.memo(({ article, onBookmark }: NewsCardProps) => {
  const [isBookmarked, setIsBookmarked] = useState(false);
  
  const handleBookmark = useCallback(() => {
    onBookmark(article.id);
    setIsBookmarked(!isBookmarked);
  }, [article.id, isBookmarked, onBookmark]);

  return (
    <div className="news-card">
      <OptimizedImage 
        src={article.image} 
        alt={article.title}
        width={300}
        height={200}
        priority={false}
      />
      <h3>{article.title}</h3>
      <p>{article.excerpt}</p>
      <button onClick={handleBookmark}>
        {isBookmarked ? "Bookmarked" : "Bookmark"}
      </button>
    </div>
  );
});

// Memoized computed values
const useNewsFeedCalculations = (articles: Article[]) => {
  const sortedArticles = useMemo(() => 
    [...articles].sort((a, b) => 
      new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
    ), [articles]
  );

  const trendingArticles = useMemo(() => 
    articles.filter(article => article.viewCount > 1000), [articles]
  );

  return { sortedArticles, trendingArticles };
};

Virtualization for large lists (react-window / react-virtual)

// Virtualized list implementation
const VirtualizedNewsFeed = ({ articles }: { articles: Article[] }) => {
  const { height, width } = useWindowSize();
  
  return (
    <List
      height={height - 100}
      itemCount={articles.length}
      itemSize={200}
      width={width - 40}
    >
      {({ index, style }) => (
        <div style={style}>
          <NewsCard article={articles[index]} />
        </div>
      )}
    </List>
  );
};

// Virtualized infinite scroll implementation
const InfiniteNewsFeed = () => {
  const [articles, setArticles] = useState<Article[]>([]);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const loadMore = useCallback(async () => {
    if (loading || !hasMore) return;
    
    setLoading(true);
    try {
      const newArticles = await fetchNewsFeed(
        articles.length, 
        20
      );
      
      if (newArticles.length === 0) {
        setHasMore(false);
        return;
      }
      
      setArticles(prev => [...prev, ...newArticles]);
    } finally {
      setLoading(false);
    }
  }, [articles.length, loading, hasMore]);

  return (
    <InfiniteScroll
      dataLength={articles.length}
      next={loadMore}
      hasMore={hasMore}
      loader={<LoadingSpinner />}
      endMessage={<p>No more articles</p>}
    >
      <VirtualizedNewsFeed articles={articles} />
    </InfiniteScroll>
  );
};

Prefetching and preloading strategy

// Prefetching strategy for news feed
const NewsFeedPreloader = () => {
  const prefetchNextPage = useCallback(() => {
    // Preload next page of articles
    const nextArticlesPromise = fetchNewsFeed(
      currentPage * pageSize, 
      pageSize
    );
    
    // Store in cache for immediate use
    cache.set(`articles_page_${currentPage + 1}`, nextArticlesPromise);
    
    // Preload critical images
    const imagePreloads = articles.slice(0, 5).map(article => 
      new Image().src = article.image
    );
    
    return Promise.all(imagePreloads);
  }, [currentPage, pageSize, articles]);

  useEffect(() => {
    // Prefetch when user is likely to navigate
    const prefetchTimer = setTimeout(prefetchNextPage, 3000);
    
    return () => clearTimeout(prefetchTimer);
  }, [prefetchNextPage]);

  return null;
};

Performance monitoring strategy

// Performance tracking hook
const usePerformanceMonitor = () => {
  useEffect(() => {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'largest-contentful-paint') {
          // Track LCP for analytics
          trackMetric('lcp', entry.startTime);
        }
        
        if (entry.entryType === 'layout-shift') {
          // Track CLS for stability
          trackMetric('cls', entry.value);
        }
      }
    });

    observer.observe({ entryTypes: ['largest-contentful-paint', 'layout-shift'] });
    
    return () => observer.disconnect();
  }, []);
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

graph TD
    A[NewsFeedContainer] --> B[NewsCard]
    A --> C[NewsList]
    A --> D[ErrorBoundary]
    B --> E[ArticleImage]
    B --> F[ArticleContent]
    C --> G[NewsCard]
    D --> H[ErrorFallback]

Error boundaries will be strategically placed at:

  • NewsFeedContainer - Catches cascading errors from the entire feed
  • NewsCard - Handles individual article rendering failures
  • ArticleImage - Isolated image loading errors

Global Error Handler Design

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

export interface ErrorHandlerConfig {
  logToConsole?: boolean;
  reportToSentry?: boolean;
  fallbackComponent?: React.ComponentType<{ error: Error }>;
}

export class GlobalErrorHandler {
  private config: ErrorHandlerConfig;

  constructor(config: ErrorHandlerConfig = {}) {
    this.config = {
      logToConsole: true,
      reportToSentry: true,
      fallbackComponent: DefaultErrorFallback,
      ...config
    };
  }

  handleError(error: Error, errorInfo?: React.ErrorInfo): void {
    if (this.config.logToConsole) {
      console.error('Unhandled error:', error, errorInfo);
    }

    if (this.config.reportToSentry) {
      captureException(error, {
        contexts: { react: errorInfo }
      });
    }

    // Notify analytics service
    this.trackError(error, errorInfo);
  }

  private trackError(error: Error, errorInfo?: React.ErrorInfo): void {
    // Track in analytics service
  }
}

const globalErrorHandler = new GlobalErrorHandler();

Offline / Network Failure Handling

// 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/NewsFeedContainer.tsx
export const NewsFeedContainer = () => {
  const { isOnline } = useNetworkStatus();
  const [error, setError] = useState<Error | null>(null);
  
  if (!isOnline) {
    return <OfflineFallback />;
  }

  if (error) {
    return <ErrorFallback error={error} />;
  }

  return <NewsFeedList />;
};

Graceful Degradation Patterns

// src/components/NewsCard.tsx
interface NewsCardProps {
  article: Article;
  onRetry?: () => void;
}

export const NewsCard = ({ article, onRetry }: NewsCardProps) => {
  const [imageError, setImageError] = useState(false);
  const [isImageLoading, setIsImageLoading] = useState(true);

  const handleImageLoad = () => {
    setIsImageLoading(false);
  };

  const handleImageError = () => {
    setImageError(true);
    setIsImageLoading(false);
  };

  if (isImageLoading) {
    return <SkeletonCard />;
  }

  return (
    <div className="news-card">
      {imageError ? (
        <div className="fallback-image">Image Unavailable</div>
      ) : (
        <img 
          src={article.image} 
          alt={article.title}
          onLoad={handleImageLoad}
          onError={handleImageError}
        />
      )}
      <div className="article-content">
        <h3>{article.title}</h3>
        <p>{article.description}</p>
      </div>
    </div>
  );
};

User Feedback on Errors

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

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

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

  const addToast = (toast: Omit<Toast, 'id'>) => {
    const newToast = {
      ...toast,
      id: Date.now().toString()
    };
    
    setToasts(prev => [...prev, newToast]);
    
    if (toast.duration !== 0) {
      setTimeout(() => {
        removeToast(newToast.id);
      }, toast.duration || 5000);
    }
  };

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

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

// src/components/Toast.tsx
interface ToastProps {
  toast: Toast;
  onRemove: (id: string) => void;
}

const Toast = ({ toast, onRemove }: ToastProps) => {
  const [isVisible, setIsVisible] = useState(true);

  useEffect(() => {
    if (!isVisible) {
      setTimeout(() => onRemove(toast.id), 300);
    }
  }, [isVisible, onRemove, toast.id]);

  return (
    <div 
      className={`toast toast--${toast.type} ${isVisible ? 'toast--visible' : ''}`}
      role="alert"
      aria-live="polite"
    >
      <span className="toast__message">{toast.message}</span>
      <button 
        className="toast__close"
        onClick={() => setIsVisible(false)}
        aria-label="Close notification"
      >
        ×
      </button>
    </div>
  );
};

Error Fallback Components

// src/components/ErrorFallback.tsx
interface ErrorFallbackProps {
  error: Error;
  resetErrorBoundary?: () => void;
}

export const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => {
  return (
    <div className="error-container" role="alert">
      <h2>Something went wrong</h2>
      <p>We're sorry, but we couldn't load the news feed.</p>
      
      {process.env.NODE_ENV === 'development' && (
        <details className="error-details">
          <summary>Details</summary>
          <pre>{error.message}</pre>
        </details>
      )}
      
      <div className="error-actions">
        <button 
          onClick={resetErrorBoundary}
          className="btn btn-primary"
        >
          Retry Loading
        </button>
        
        <button 
          onClick={() => window.location.reload()}
          className="btn btn-secondary"
        >
          Refresh Page
        </button>
      </div>
    </div>
  );
};

// src/components/OfflineFallback.tsx
export const OfflineFallback = () => {
  return (
    <div className="offline-container" role="alert">
      <h2>No Internet Connection</h2>
      <p>You're currently offline. Please check your connection and try again.</p>
      
      <div className="offline-actions">
        <button 
          onClick={() => window.location.reload()}
          className="btn btn-primary"
        >
          Try Again
        </button>
      </div>
    </div>
  );
};

8. Testing Strategy

Unit Testing

Component Tests (70% coverage target):

  • NewsFeed component with mocked data and loading states
  • ArticleCard component with different content types (image, video, text)
  • LoadingSkeleton and EmptyState components
  • Pagination component with boundary state handling
  • SearchBar and FilterBar components with input validation

Utility Tests (85% coverage target):

  • Data transformation utilities (API response to domain model mapping)
  • Date formatting and relative time calculation
  • Search and filter logic implementations
  • Image URL processing and fallback handling

Integration Testing Strategy

Feature Integration Tests (50% coverage target):

  • End-to-end flow from API fetch → state update → UI rendering
  • Search + filter combination scenarios
  • Infinite scroll loading behavior with mock API responses
  • Error state transitions and recovery flows

Cross-component Interaction Tests:

  • NewsFeed → ArticleCard communication (click handlers, read status updates)
  • FilterBar → NewsFeed state synchronization
  • SearchBar → NewsFeed search query propagation

E2E Testing Scope and Tool

Tool: Playwright (selected over Cypress for better TypeScript support and parallel test execution)

Covered User Journeys:

  1. Loading news feed with 20 articles
  2. Searching for specific keywords across multiple articles
  3. Filtering by category (Technology, Sports, Politics)
  4. Infinite scroll loading more articles
  5. Error recovery from network failures
  6. User interaction flow (mark as read, save article)

Visual Regression Testing

  • Tool: Storybook with Chromatic integration
  • Coverage: All article card variants, loading states, error states, empty states
  • Breakpoint testing: Mobile (375px), Tablet (768px), Desktop (1200px) widths

Accessibility Testing Automation

Tool: axe-core with Jest integration (automated in CI pipeline)

  • Coverage: WCAG 2.1 AA compliance checks for all interactive elements
  • Focus management: Keyboard navigation flow validation
  • Screen reader compatibility: ARIA attribute verification

Test Data Strategy and Fixtures

Mock Data Structure:

interface MockArticle {
  id: string;
  title: string;
  content: string;
  author: { name: string; avatar: string };
  category: string;
  publishedAt: string;
  imageUrl?: string;
  readTime: number;
  isRead: boolean;
  isSaved: boolean;
}

Test Data Categories:

  • 100+ realistic article fixtures with varied content types
  • Edge case scenarios (empty content, missing images, malformed dates)
  • Multi-language content for i18n testing
  • Performance test data (500+ articles for scroll performance)

Test Data Generation:

  • Factory pattern for generating consistent test data
  • Randomized content generation with deterministic seeds for reproducible tests
  • API response mock generator that matches actual backend schema

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The News Feed UI must meet WCAG 2.1 AA standards with the following key requirements:

  • Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background (minimum 3:1 for large text)
  • Keyboard Navigation: Full keyboard accessibility for all interactive elements (focus order follows logical reading flow)
  • Screen Reader Support: Proper ARIA roles and semantic HTML for content structure
  • Focus Management: Clear focus indicators and proper focus trapping in modals
  • Alternative Text: All images must have descriptive alt text or be marked as decorative
  • Skip Links: Skip to content link for keyboard users to bypass navigation
  • Error Handling: Clear error messages with proper ARIA attributes for screen readers

Keyboard navigation support

All interactive elements must be fully navigable via keyboard:

  • Tab Navigation: Logical tab order through all feed items, action buttons, and controls
  • Enter/Space Activation: All interactive elements (cards, buttons) must respond to Enter and Space keys
  • Escape Handling: Modal close functionality via Escape key
  • Arrow Navigation: Support for navigating through news items using arrow keys (when appropriate)
  • Focus Indicators: Visible focus rings that comply with WCAG standards

Focus management strategy

stateDiagram-v2
    [*] --> idle
    idle --> focused_item : keyboard nav
    focused_item --> action_button : tab to action
    action_button --> focused_item : back to item
    focused_item --> modal_open : open details
    modal_open --> modal_closed : escape key
    modal_closed --> idle : close modal
  • Focus Trapping: When modals open, focus is trapped within the modal content
  • Focus Restoration: Focus returns to the previous element when modals close
  • Programmatic Focus: Use focus() API for keyboard-driven navigation between items
  • Skip Links: Implement skip-to-content link at the top of the page for keyboard users

ARIA roles and attributes plan

// NewsItem component ARIA implementation
interface NewsItemProps {
  id: string;
  title: string;
  summary: string;
  publishedAt: string;
  isBookmarked?: boolean;
  onBookmark?: () => void;
  onOpenDetails?: () => void;
}

const NewsItem: React.FC<NewsItemProps> = ({ 
  id, 
  title, 
  summary, 
  publishedAt,
  isBookmarked,
  onBookmark,
  onOpenDetails
}) => {
  return (
    <article 
      role="article"
      aria-labelledby={`title-${id}`}
      aria-describedby={`summary-${id}`}
      tabIndex={0}
      className="news-item"
    >
      <header>
        <h2 
          id={`title-${id}`} 
          className="news-title"
          tabIndex={-1}
        >
          {title}
        </h2>
        <time 
          dateTime={publishedAt} 
          className="publish-date"
          aria-label={`Published on ${publishedAt}`}
        >
          {publishedAt}
        </time>
      </header>
      
      <p 
        id={`summary-${id}`} 
        className="news-summary"
      >
        {summary}
      </p>
      
      <div className="news-actions">
        <button
          aria-label={isBookmarked ? "Remove bookmark" : "Bookmark article"}
          onClick={onBookmark}
          className="bookmark-button"
          aria-pressed={isBookmarked}
        >
          {isBookmarked ? "★" : "☆"}
        </button>
        
        <button
          aria-label={`View details for ${title}`}
          onClick={onOpenDetails}
          className="details-button"
        >
          View Details
        </button>
      </div>
    </article>
  );
};

Screen reader compatibility

  • Semantic HTML: Use proper heading hierarchy (h2 for titles, p for summaries)
  • Live Regions: Use aria-live="polite" for feed updates that don't require immediate attention
  • Dynamic Content: Announce new items added to the feed with aria-live="assertive"
  • Role Descriptions: Use role="feed" on the main container to indicate list-like content
  • Landmark Regions: Proper use of main, article, and nav landmarks

Keyboard navigation implementation

// Keyboard navigation hook for news feed
const useNewsFeedKeyboardNavigation = (items: NewsItem[], onItemSelect: (item: NewsItem) => void) => {
  const [focusedIndex, setFocusedIndex] = useState(0);
  
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'ArrowDown') {
        setFocusedIndex(prev => Math.min(prev + 1, items.length - 1));
      } else if (e.key === 'ArrowUp') {
        setFocusedIndex(prev => Math.max(prev - 1, 0));
      } else if (e.key === 'Enter' || e.key === ' ') {
        if (items[focusedIndex]) {
          onItemSelect(items[focusedIndex]);
        }
      } else if (e.key === 'Home') {
        setFocusedIndex(0);
      } else if (e.key === 'End') {
        setFocusedIndex(items.length - 1);
      }
    };
    
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [items, focusedIndex, onItemSelect]);
  
  return { focusedIndex, setFocusedIndex };
};

Focus trap implementation

// Focus trap for modal dialogs
const useFocusTrap = (modalRef: React.RefObject<HTMLDivElement>) => {
  useEffect(() => {
    const handleFocusTrap = (e: KeyboardEvent) => {
      if (!modalRef.current) return;
      
      const focusableElements = modalRef.current.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      const firstElement = focusableElements[0] as HTMLElement;
      const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
      
      if (e.key === 'Tab') {
        if (document.activeElement === lastElement && !e.shiftKey) {
          e.preventDefault();
          firstElement.focus();
        } else if (document.activeElement === firstElement && e.shiftKey) {
          e.preventDefault();
          lastElement.focus();
        }
      }
    };
    
    window.addEventListener('keydown', handleFocusTrap);
    return () => window.removeEventListener('keydown', handleFocusTrap);
  }, [modalRef]);
};

ARIA live region for feed updates

// Feed update announcement component
const FeedUpdateAnnouncer: React.FC<{ 
  count: number; 
  action: 'added' | 'removed' | 'updated' 
}> = ({ count, action }) => {
  const message = `Feed updated: ${count} ${action === 'added' ? 'new articles' : 
    action === 'removed' ? 'articles removed' : 'articles updated'}`;
  
  return (
    <div 
      aria-live="polite" 
      aria-atomic="true"
      className="sr-only"
    >
      {message}
    </div>
  );
};

Skip to content implementation

// Skip link component for keyboard navigation
const SkipToContentLink: React.FC = () => {
  const handleSkipClick = (e: React.MouseEvent) => {
    e.preventDefault();
    const mainContent = document.getElementById('main-content');
    if (mainContent) {
      mainContent.focus();
    }
  };
  
  return (
    <a 
      href="#main-content" 
      onClick={handleSkipClick}
      className="skip-link"
      aria-label="Skip to main content"
    >
      Skip to content
    </a>
  );
};

Keyboard focus styling

/* Focus styles that meet WCAG requirements */
.news-item:focus {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

.news-item:focus:not(:focus-visible) {
  outline: none;
}

/* Focus-visible for better UX */
.news-item:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

/* Screen reader only class */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Accessibility testing considerations

  • Automated Testing: Use axe-core for automated accessibility checks in CI pipeline
  • Manual Testing: Keyboard-only navigation testing across all supported browsers
  • Screen Reader Testing: Verify compatibility with NVDA, JAWS, and VoiceOver
  • Color Contrast Testing: Validate all color combinations meet 4.5:1 contrast ratio
  • Dynamic Content Announcements: Ensure screen readers properly announce feed updates

Keyboard navigation flow for news feed

  1. Initial Load: Focus lands on first news item
  2. Navigation: Arrow keys move focus between items, Home/End to jump to start/end
  3. Interaction: Enter/Space activates item details or bookmark actions
  4. Modal Interaction: Escape key closes modals, Tab cycles through modal controls
  5. Focus Restoration: When returning to feed, focus returns to last focused item

Screen reader announcement patterns

  • Feed Updates: "New articles added to feed"
  • Bookmark Changes: "Article bookmarked" or "Bookmark removed"
  • Loading States: "Loading news feed, please wait"
  • Empty States: "No articles found in your feed"

The accessibility implementation ensures that users with disabilities can navigate and interact with the News Feed UI using keyboard, screen readers, and other assistive technologies while maintaining full functionality and semantic meaning.

10. Security Considerations

XSS Prevention Strategy

The news feed must prevent cross-site scripting attacks by sanitizing all user-generated content and dynamic HTML. This includes:

  • Content sanitization: All article titles, descriptions, user comments, and tags must be sanitized using a robust library like DOMPurify before rendering
  • HTML escaping: User input should never be directly inserted into HTML attributes or DOM properties without proper escaping
  • Attribute validation: All dynamic attributes (href, src, etc.) must be validated against a whitelist of allowed protocols (https:, data: for images, etc.)
// Example sanitization approach
const sanitizeHTML = (html: string): string => {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'title', 'alt'],
    ALLOWED_URI_REGEXP: /^https?:\/\/[^\s]+$/,
  });
};

CSRF Protection

To prevent cross-site request forgery attacks on user actions (like liking articles or commenting):

  • CSRF tokens: All authenticated API requests must include a CSRF token in headers or request body
  • Token rotation: Tokens should be rotated per session and invalidated after user inactivity
  • SameSite cookies: Session cookies must have SameSite=Strict or SameSite=Lax attributes

Content Security Policy (CSP) Headers

Implement strict CSP headers to prevent injection attacks:

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
  style-src 'self' 'unsafe-inline'; 
  img-src 'self' data: https:; 
  font-src 'self' data:; 
  connect-src 'self' https://api.newsfeed.com;

Sensitive Data Handling in Frontend State

  • Authentication tokens: Never store auth tokens in localStorage or cookies accessible via JavaScript
  • User session data: Only keep minimal user context in state, avoid storing sensitive user metadata
  • API keys: Never expose API keys in frontend code or console logs

Input Sanitization Approach

All user inputs (search queries, comment fields, filters) must be sanitized:

const sanitizeInput = (input: string): string => {
  // Remove dangerous characters and patterns
  return input
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .trim();
};

const validateSearchQuery = (query: string): boolean => {
  // Enforce length limits and character restrictions
  return query.length <= 200 && 
         !/[\x00-\x1F\x7F]/.test(query) && // Control characters
         !/<script[^>]*>.*?<\/script>/i.test(query); // Script tags
};

Security Headers Implementation

// Middleware or server-side headers configuration
const securityHeaders = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'Referrer-Policy': 'strict-origin-when-cross-origin'
};

Third-party Library Security

All third-party dependencies must be:

  • Regularly audited for security vulnerabilities
  • Updated to latest secure versions
  • Scanned for known vulnerabilities using tools like npm audit or Snyk
  • Avoiding libraries with known security issues (e.g., vulnerable versions of lodash or react)

Secure API Communication

  • HTTPS enforcement: All API calls must use HTTPS only
  • Request signing: For sensitive operations, implement request signature verification
  • Rate limiting: Prevent abuse through API rate limiting on client-side requests

Data Exposure Prevention

  • Field-level filtering: Never expose sensitive fields in API responses to unauthorized users
  • Response size limits: Prevent data flooding attacks by limiting response sizes
  • Pagination security: Validate page numbers and limit maximum page size to prevent DoS attacks

Authentication & Authorization

  • Role-based access control: Ensure only authorized users can perform actions like editing articles or deleting comments
  • Token refresh mechanisms: Implement secure token refresh without exposing tokens in logs or URLs
  • Session management: Proper session timeout and invalidation on logout or token expiration

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following the BEM-style naming convention with feature-scoped keys:

// Translation keys structure
{
  "newsFeed": {
    "title": "News Feed",
    "emptyState": {
      "title": "No articles found",
      "description": "Try adjusting your filters"
    },
    "article": {
      "publishedDate": "{{date}} ago",
      "readTime": "{{minutes}} min read",
      "author": "By {{author}}"
    },
    "filters": {
      "category": "Category",
      "search": "Search articles"
    }
  }
}

RTL Support Considerations

RTL layouts will be handled through CSS classes and direction properties:

/* RTL support in CSS */
[dir="rtl"] .news-feed-container {
  direction: rtl;
}

[dir="rtl"] .article-content {
  text-align: right;
}

Date, Number, Currency Formatting Strategy

Using Intl API with locale-aware formatting:

// Format helpers
const formatDate = (date: Date, locale: string): string => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'short',
    day: 'numeric'
  }).format(date);
};

const formatNumber = (num: number, locale: string): string => {
  return new Intl.NumberFormat(locale, {
    notation: 'compact',
    compactDisplay: 'short'
  }).format(num);
};

Locale Detection and Switching Strategy

Automatic detection using browser language, with manual override:

// Locale detection and switching
const detectLocale = (): string => {
  const savedLocale = localStorage.getItem('preferredLocale');
  if (savedLocale) return savedLocale;
  
  const browserLang = navigator.language.split('-')[0];
  const supportedLocales = ['en', 'es', 'fr', 'de'];
  return supportedLocales.includes(browserLang) ? browserLang : 'en';
};

const switchLocale = (newLocale: string): void => {
  localStorage.setItem('preferredLocale', newLocale);
  // Trigger re-render with new locale context
};

Translation File Organization

// Translation files structure
{
  "en": {
    "newsFeed": {
      "title": "News Feed",
      "emptyState": {
        "title": "No articles found",
        "description": "Try adjusting your filters"
      }
    }
  },
  "es": {
    "newsFeed": {
      "title": "Noticias",
      "emptyState": {
        "title": "No se encontraron artículos",
        "description": "Intenta ajustar tus filtros"
      }
    }
  }
}

i18n Context Provider

// i18n context with React
interface I18nContextType {
  locale: string;
  translations: Record<string, any>;
  formatMessage: (key: string, params?: Record<string, any>) => string;
  switchLocale: (locale: string) => void;
}

const I18nContext = React.createContext<I18nContextType>({
  locale: 'en',
  translations: {},
  formatMessage: () => '',
  switchLocale: () => {}
});

Translation Key Management

All translation keys will be extracted and managed through:

  • i18next with i18next-parser for automatic key extraction
  • Translation files stored in public/locales/{locale}/translation.json
  • CI pipeline validates all translation keys exist across locales
  • Translation key naming follows feature.section.subsection pattern

Language-Specific Content Handling

Content that varies by language (e.g., date formats, currency) will be:

  • Dynamically formatted using Intl APIs
  • Resolved at runtime based on user's preferred locale
  • Fallback to English for missing translations
  • Support for pluralization and gender-specific content through i18next plugins

Testing Internationalization

Testing will include:

  • Snapshot tests for different locales
  • Translation key validation in CI pipeline
  • RTL layout verification in browser tests
  • Date/time formatting consistency checks across locales

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The News Feed UI should track key performance indicators that directly impact user experience:

  • First Contentful Paint (FCP): Target <2s for 95% of users
  • Largest Contentful Paint (LCP): Target <2.5s for 95% of users
  • Cumulative Layout Shift (CLS): Target <0.1 for 95% of users
  • First Input Delay (FID): Target <100ms for 95% of users
  • Time to First Article: Target <1s for 95% of users
  • Article Load Time: Track median and 95th percentile per article type

Error Tracking Integration

Sentry Integration Strategy:

  • Capture unhandled promise rejections and global errors
  • Track React error boundaries with context-specific metadata
  • Log API request failures with full request/response context
  • Monitor client-side performance regressions with threshold alerts

Custom Events and User Journey Tracking

Core User Journey Events:

// Event tracking interface
interface UserEvent {
  eventName: string;
  timestamp: number;
  userId?: string;
  articleId?: string;
  viewportPosition?: number;
  scrollDepth?: number;
  interactionType?: 'click' | 'scroll' | 'load' | 'view';
  metadata?: Record<string, any>;
}

// Example event tracking
const trackUserEvent = (event: UserEvent) => {
  // Send to analytics service or RUM provider
  console.log('Tracking event:', event);
};

Key Events to Track:

  • Article view (LCP, scroll depth)
  • Article click (CTA engagement)
  • Feed refresh (pull-to-refresh or manual)
  • Load more articles (infinite scroll)
  • User session start/end
  • Time spent per article

Performance Monitoring and Regression Detection

Performance Baseline Strategy:

  • Establish baseline metrics for each article type (text, image, video)
  • Implement automated regression detection with 5% tolerance thresholds
  • Set up alerts for performance degradation >10% from baseline

Monitoring Implementation:

// Performance monitoring hook
const usePerformanceMonitor = () => {
  const [metrics, setMetrics] = useState<PerformanceMetrics>({
    fcp: 0,
    lcp: 0,
    cls: 0,
    fid: 0
  });

  useEffect(() => {
    // Setup performance observer for Core Web Vitals
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.name === 'first-contentful-paint') {
          setMetrics(prev => ({ ...prev, fcp: entry.startTime }));
        }
        if (entry.name === 'largest-contentful-paint') {
          setMetrics(prev => ({ ...prev, lcp: entry.startTime }));
        }
      });
    });

    observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
    
    return () => observer.disconnect();
  }, []);

  return metrics;
};

Alerting Strategy:

  • Set up dashboard alerts for performance regressions
  • Implement automated incident response for critical thresholds
  • Track error rates and implement automatic scaling alerts

Observability Tooling Stack

Monitoring Tools:

  • Sentry: For error tracking and performance monitoring
  • LogRocket: Session replay for user behavior analysis
  • Lighthouse: Automated performance testing
  • Custom RUM: For application-specific metrics

Dashboard Requirements:

  • Real-time feed performance dashboard
  • User engagement metrics (time spent, scroll depth)
  • Error rate and latency trends
  • A/B testing performance comparison

Data Retention:

  • 30-day retention for detailed session data
  • 90-day retention for aggregated metrics
  • 1-year retention for historical trend analysis

Integration with Existing Monitoring Infrastructure

The News Feed monitoring should integrate seamlessly with existing observability tools:

  • Use existing logging infrastructure (e.g., Winston, Bunyan)
  • Follow company's telemetry standards for metric naming and tagging
  • Support centralized alerting and dashboarding systems
  • Implement structured logging with consistent error context

Rollout Strategy

Phase 1: Basic RUM and error tracking Phase 2: Advanced performance metrics and user journey tracking
Phase 3: A/B testing integration and custom dashboard development

This monitoring strategy ensures comprehensive visibility into the News Feed's performance and user experience, enabling data-driven improvements and rapid incident response.

13. File & Folder Structure

Proposed Directory Structure

/src
├── components/
│   ├── news-feed/
│   │   ├── NewsFeed.tsx
│   │   ├── NewsCard.tsx
│   │   ├── NewsCardSkeleton.tsx
│   │   └── index.ts
├── features/
│   ├── news-feed/
│   │   ├── index.ts
│   │   ├── hooks/
│   │   │   └── useNewsFeed.ts
│   │   ├── store/
│   │   │   └── newsFeedSlice.ts
│   │   ├── types/
│   │   │   └── NewsItem.ts
│   │   └── api/
│   │       └── newsApi.ts
├── hooks/
│   ├── useInfiniteScroll.ts
│   └── useDebounce.ts
├── types/
│   ├── index.ts
│   └── common.ts
├── utils/
│   ├── formatDate.ts
│   └── sanitizeHtml.ts
└── pages/
    └── NewsFeedPage.tsx

Module Boundaries and Import Rules

  • Feature-based structure chosen for better encapsulation and maintainability
  • No cross-feature imports allowed to prevent tight coupling
  • Barrel exports (index.ts) for clean component imports
  • Shared utilities and types live in /src/types and /src/utils
  • Feature-specific code is isolated under /features/news-feed/

Shared vs. Feature-Specific Code Separation

Shared code:

  • Common types (common.ts)
  • Utility functions (formatDate.ts, sanitizeHtml.ts)
  • Reusable components (NewsCardSkeleton.tsx)

Feature-specific code:

  • News feed business logic in features/news-feed/
  • Component implementations in components/news-feed/
  • API integration and data transformation logic

This structure enables clear ownership boundaries, easy refactoring, and prevents circular dependencies between features.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

React Query (TanStack Query)

  • Justification: Excellent data fetching and state management for server state, with built-in caching, background updates, and automatic refetching. Provides superior developer experience over custom solutions for complex data flows.
  • Bundle impact: ~15KB gzipped
  • Alternatives: Redux Toolkit, Zustand - both require more boilerplate for data fetching and caching

React Router v6

  • Justification: Modern, declarative routing with nested routes, route-based code splitting, and excellent TypeScript support. Supports dynamic route parameters and layout routes.
  • Bundle impact: ~12KB gzipped
  • Alternatives: Reach Router - less actively maintained and more complex for nested routing

Framer Motion

  • Justification: Production-ready animation library with excellent performance, built-in layout animations, and seamless React integration. Enables smooth transitions between feed states and content loading.
  • Bundle impact: ~8KB gzipped
  • Alternatives: CSS animations - less maintainable and harder to coordinate with React state

date-fns

  • Justification: Modular date formatting library that's tree-shakeable, highly performant, and supports internationalization. Better than moment.js for performance and bundle size.
  • Bundle impact: ~2KB gzipped
  • Alternatives: Luxon, day.js - both have larger bundles or less mature internationalization support

react-infinite-scroll-component

  • Justification: Optimized infinite scroll implementation that handles edge cases like loading states, empty lists, and proper scroll restoration. Reduces boilerplate for feed pagination.
  • Bundle impact: ~3KB gzipped
  • Alternatives: Custom implementations - prone to memory leaks and poor scroll performance

Alternatives Considered and Rejected

Redux Toolkit

  • Rejected: Too much boilerplate for simple data fetching scenarios. React Query already handles most use cases with better caching and automatic refetching.

Axios

  • Rejected: While popular, fetch-based approach with React Query provides better control over caching and request lifecycle management.

Styled Components

  • Rejected: CSS Modules or Tailwind provide better performance and maintainability for large-scale applications with consistent design systems.

Licensing and Bundle Size Impact Assessment

Core bundle size budget: ~40KB (excluding React, React Router, and React Query which are already in the app)

Tree-shaking benefits: All libraries support tree-shaking for minimal bundle impact. React Query and date-fns are particularly modular.

License compatibility: All selected libraries use MIT or BSD-3-Clause licenses, compatible with commercial applications. No GPL dependencies.

Performance impact: All libraries are optimized for production use and support lazy loading where appropriate. Bundle size is within acceptable limits for mobile-first design.

Maintenance strategy: Libraries with active maintainers and strong community support (React Query, React Router) ensure long-term viability and security updates.

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Content personalization vs. algorithmic curation: Should the news feed prioritize user preferences or algorithmic relevance? This decision impacts the complexity of state management and API requirements.
  • Real-time vs. batched updates: How frequently should the feed refresh? Real-time updates require WebSockets or long polling, while batched updates are more scalable but less responsive.
  • Content moderation strategy: Should content be pre-moderated or handled through user reporting? This affects the API surface and error handling patterns.
  • Infinite scroll vs. pagination: The choice between infinite scroll and pagination impacts performance, SEO, and user experience consistency.

Known trade-offs made in this design

  • Performance vs. feature richness: Prioritized core rendering performance over advanced UI features like complex animations or real-time content updates to maintain acceptable LCP and CLS targets.
  • State management complexity vs. scalability: Used a hybrid approach (Context API + React Query) to balance developer velocity with long-term maintainability, rather than a single monolithic solution.
  • Client-side vs. server-side rendering: Chose CSR for faster initial load and better user interactivity, at the cost of SEO optimization and first-contentful paint.
  • Component reusability vs. specificity: Created highly specific components for news feed items to optimize rendering performance, potentially reducing code reuse.

Risks and mitigations

  • Performance degradation with large feeds: Risk of poor CLS and LCP with thousands of items. Mitigation includes virtualization, content deduplication, and prefetching strategies.
  • API rate limiting and failure: Risk of service unavailability or rate limiting. Mitigation includes request batching, exponential backoff, and proper error boundaries.
  • Content freshness vs. caching: Risk of stale content or excessive cache invalidation. Mitigation includes smart cache TTLs and optimistic updates with fallbacks.
  • User engagement vs. content quality: Risk of algorithmic bias or low-quality content promotion. Mitigation includes A/B testing, user feedback loops, and content quality metrics.

Technical debt acknowledged

  • Hardcoded content types: News feed items currently assume a fixed content structure, making future content type extensions more complex.
  • Limited offline support: The design assumes online connectivity for all operations, with minimal fallback strategies for offline scenarios.
  • Monolithic state management: The current approach mixes local and global state in ways that could complicate future scaling or migration to more sophisticated state management solutions.
  • Limited accessibility testing coverage: While A11y considerations are included, comprehensive end-to-end accessibility testing is not fully implemented in this design.

Was this article helpful?

Topics in this article

More from Frontend LLD

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.