Skip to main content

🎯 Module 8 Project: Social Media Feed Application

Congratulations on completing Module 8! You've mastered state management with Zustand and Redux Toolkit, learned powerful data fetching with React Query, and discovered professional architecture patterns that separate great applications from good ones. Now it's time to put everything together. In this comprehensive capstone project, you'll build a production-ready social media feed that demonstrates world-class React architecture. This isn't just practiceβ€”it's a portfolio piece that proves you can build real, scalable applications.

🎯 Project Objectives

By completing this project, you will demonstrate your mastery of:

  • Feature-based folder structure for scalable applications
  • React Query for server state management (posts, comments, users)
  • Zustand for client state management (UI state, notifications)
  • Service layer pattern for clean API abstraction
  • Custom hooks for reusable business logic
  • Component organization (Container/Presentational patterns)
  • Optimistic updates for instant user feedback
  • Comprehensive error handling architecture
  • TypeScript throughout with proper type organization
  • Production-ready code organization and patterns

Estimated Time: 6-10 hours (or spread over multiple sessions)

Difficulty: Advanced

πŸ“‘ Project Guide

πŸ“‹ Project Overview

You'll build a modern social media feed application with real-time interactions, similar to Twitter/X or Facebook. This project demonstrates every architectural pattern you've learned in Module 8.

graph TB A[Social Media Feed] --> B[Posts Feature] A --> C[Comments Feature] A --> D[Users Feature] A --> E[Auth Feature] B --> F[Post List] B --> G[Create Post] B --> H[Like Post] B --> I[Share Post] C --> J[Comment List] C --> K[Add Comment] C --> L[Reply to Comment] D --> M[User Profile] D --> N[Follow/Unfollow] D --> O[User Stats] E --> P[Login/Signup] E --> Q[Auth State] style A fill:#667eea,stroke:#333,stroke-width:3px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style C fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff style D fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff style E fill:#9C27B0,stroke:#333,stroke-width:2px,color:#fff

What You'll Build

🌟 Core Features

1. Posts Feed
  • Infinite scroll feed with pagination
  • Create new posts with text and images
  • Edit and delete your own posts
  • Like/unlike posts with optimistic updates
  • Share posts (simulated)
  • Post timestamp and author information
  • Loading states and skeleton screens
2. Comments System
  • View comments on any post
  • Add new comments
  • Nested replies to comments
  • Edit and delete your comments
  • Like comments
  • Real-time comment count updates
3. User Interactions
  • User profiles with bio and stats
  • Follow/unfollow users
  • View user's posts
  • Follower/following counts
  • User avatars and display names
4. Authentication
  • Login and signup forms
  • JWT token management
  • Protected routes
  • Auth state with Zustand
  • Persistent login
5. UI/UX Features
  • Responsive mobile-first design
  • Toast notifications for actions
  • Loading skeletons
  • Error boundaries
  • Optimistic UI updates
  • Smooth animations

πŸŽ“ What You'll Learn

  • Real-World Architecture: See how professional applications are structured
  • State Management Patterns: When to use React Query vs Zustand
  • Scalable Code Organization: Feature folders that grow with your app
  • Performance Optimization: Caching, optimistic updates, lazy loading
  • Error Handling: Graceful degradation at every layer
  • TypeScript Best Practices: Type safety throughout the stack
  • Component Patterns: Container/Presentational separation
  • API Integration: Clean service layer abstractions

Technology Stack

Category Technology Purpose
Framework React 18 + TypeScript Core application framework
Server State React Query (TanStack Query) Posts, comments, users data fetching
Client State Zustand Auth state, UI state, notifications
Routing React Router Navigation and protected routes
Forms React Hook Form + Zod Post creation, comments, auth forms
HTTP Client Axios API requests with interceptors
UI/Styling CSS Modules / Tailwind Component styling
Icons Lucide React UI icons
Notifications React Hot Toast Toast notifications

✨ Features and Requirements

Functional Requirements

βœ… Must-Have Features

Posts Management
  • View infinite scroll feed of posts
  • Create posts with text content (minimum 1 character, max 280)
  • Edit own posts within 5 minutes of creation
  • Delete own posts with confirmation
  • Like/unlike posts with instant feedback
  • View like count and list of users who liked
  • Display post author with avatar and name
  • Show relative timestamps (e.g., "2 hours ago")
Comments System
  • View all comments on a post
  • Add new comment to post
  • Reply to existing comments (one level deep)
  • Edit own comments
  • Delete own comments
  • Like comments
  • Display comment count on posts
User Features
  • View user profiles with bio and stats
  • Follow/unfollow other users
  • View follower and following counts
  • See user's posts on their profile
  • Update own profile (name, bio, avatar)
Authentication
  • Sign up with email and password
  • Log in with credentials
  • Log out functionality
  • Persist auth state across page refreshes
  • Protected routes (redirects to login)
  • Public routes (feed visible to all)

🎨 Nice-to-Have Features (Stretch Goals)

  • Image upload for posts and profile pictures
  • Search functionality for posts and users
  • Hashtag support and trending topics
  • Notifications for likes and comments
  • Dark mode toggle
  • Share post to clipboard
  • Report inappropriate content
  • Block users

Technical Requirements

πŸ—οΈ Architecture Requirements

  • Feature-Based Structure: Organize code by feature (posts, comments, users, auth)
  • Service Layer: All API calls in dedicated service files
  • Custom Hooks: Business logic extracted into reusable hooks
  • Type Safety: Full TypeScript coverage, no any types
  • Error Handling: Custom error classes and error boundaries
  • Component Separation: Container and Presentational components
  • Code Reusability: Shared components in shared/ folder
  • State Management: React Query for server state, Zustand for client state

πŸ’» Implementation Requirements

  • React Query:
    • Configure QueryClient with sensible defaults
    • Implement query keys factory
    • Use mutations for all data modifications
    • Implement optimistic updates for likes
    • Proper cache invalidation strategies
  • Zustand:
    • Auth store with login/logout actions
    • UI store for modals and notifications
    • Persist auth state to localStorage
    • Typed store with proper TypeScript
  • Error Handling:
    • Custom error classes (ApiError, AuthError, ValidationError)
    • Axios interceptors for global error handling
    • Error boundaries for component errors
    • User-friendly error messages
  • Performance:
    • Lazy loading for routes
    • Infinite scroll with React Query
    • Memoization where appropriate
    • Optimistic UI updates

Non-Functional Requirements

Requirement Description
Responsive Design Mobile-first, works on all screen sizes (320px+)
Accessibility Proper ARIA labels, keyboard navigation, screen reader support
Performance Fast initial load, smooth scrolling, no janky animations
Error Handling Graceful degradation, helpful error messages
Code Quality Clean, well-organized, commented where necessary
Type Safety No TypeScript errors, proper typing throughout

πŸ›οΈ Architecture Design

This project demonstrates professional React architecture. Understanding the design decisions will help you build scalable applications in your career.

System Architecture Overview

graph TB subgraph "Presentation Layer" A[Pages/Routes] B[Layout Components] C[Feature Components] D[Shared UI Components] end subgraph "Business Logic Layer" E[Custom Hooks] F[React Query Hooks] G[Zustand Stores] end subgraph "Data Access Layer" H[API Services] I[Axios Instance] J[Error Interceptors] end subgraph "External" K[REST API] L[LocalStorage] end A --> B A --> C C --> D C --> E C --> F C --> G E --> F F --> H G --> L H --> I I --> J J --> K style A fill:#e3f2fd style E fill:#fff3cd style H fill:#c8e6c9 style K fill:#ffccbc

State Management Strategy

πŸ“Š When to Use What

State Type Tool Examples
Server State React Query Posts, comments, users, followers
Global Client State Zustand Auth state, current user, theme
UI State (Global) Zustand Modal open/closed, toast notifications
UI State (Local) useState Form inputs, dropdown open, loading buttons
Form State React Hook Form Create post form, comment form, login form
URL State React Router Current page, filters, search query

Feature Architecture Pattern

Each feature follows the same architectural pattern for consistency:

features/posts/
β”œβ”€β”€ components/          # UI components
β”‚   β”œβ”€β”€ PostList.tsx    # Container component
β”‚   β”œβ”€β”€ PostCard.tsx    # Presentational component
β”‚   β”œβ”€β”€ PostForm.tsx    # Form component
β”‚   └── PostActions.tsx # Action buttons
β”œβ”€β”€ hooks/              # Custom hooks
β”‚   β”œβ”€β”€ usePosts.ts     # Query hook
β”‚   β”œβ”€β”€ useCreatePost.ts # Mutation hook
β”‚   β”œβ”€β”€ useLikePost.ts  # Mutation hook
β”‚   └── useDeletePost.ts
β”œβ”€β”€ api/                # API service
β”‚   └── postService.ts
β”œβ”€β”€ types/              # TypeScript types
β”‚   └── post.types.ts
└── index.ts            # Public API

βœ… Benefits of This Pattern

  • Scalable: Add features without cluttering existing code
  • Maintainable: All related code lives together
  • Testable: Each layer can be tested independently
  • Reusable: Hooks and services can be shared
  • Type-Safe: TypeScript ensures correctness
  • Team-Friendly: Clear ownership and boundaries

Data Flow Architecture

sequenceDiagram participant U as User participant C as Component participant H as Custom Hook participant RQ as React Query participant S as Service participant API as Backend API U->>C: Click "Like Post" C->>H: Call useLikePost() H->>RQ: mutation.mutate(postId) Note over RQ: Optimistic Update RQ->>C: Update UI immediately C->>U: Show liked state RQ->>S: Call likePost(postId) S->>API: POST /api/posts/:id/like alt Success API-->>S: 200 OK + updated post S-->>RQ: Return data RQ->>RQ: Invalidate queries RQ->>C: Refetch & confirm C->>U: Keep liked state else Error API-->>S: 500 Error S-->>RQ: Throw error RQ->>RQ: Rollback optimistic RQ->>C: Restore previous state C->>U: Show unliked + error end

Error Handling Flow

// Error handling at each layer

// 1. API Layer (Axios Interceptor)
axios.interceptors.response.use(
  response => response,
  error => {
    // Transform HTTP errors to custom errors
    if (error.response?.status === 401) {
      throw new AuthError('Please log in');
    }
    throw new ApiError(error.message);
  }
);

// 2. Service Layer
export const postService = {
  async like(postId: string) {
    // Let errors bubble up
    const response = await apiClient.post(`/posts/${postId}/like`);
    return response.data;
  }
};

// 3. Hook Layer (React Query)
export function useLikePost() {
  return useMutation({
    mutationFn: postService.like,
    onError: (error) => {
      // React Query handles rollback
      toast.error(error.message);
    }
  });
}

// 4. Component Layer
function PostCard({ post }: { post: Post }) {
  const likeMutation = useLikePost();
  
  const handleLike = () => {
    likeMutation.mutate(post.id);
    // No try-catch needed, React Query handles it
  };
  
  return (
    <button onClick={handleLike}>
      Like ({post.likes})
    </button>
  );
}

πŸ“‚ Project Structure

Here's the complete folder structure for the social media feed application. This structure scales from small projects to enterprise applications.

Complete Folder Tree

social-media-feed/
β”œβ”€β”€ public/
β”‚   └── favicon.png
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ features/               # Feature modules
β”‚   β”‚   β”œβ”€β”€ auth/              # Authentication
β”‚   β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ LoginForm.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ SignupForm.tsx
β”‚   β”‚   β”‚   β”‚   └── ProtectedRoute.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useLogin.ts
β”‚   β”‚   β”‚   β”‚   └── useSignup.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”‚   └── authService.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ store/
β”‚   β”‚   β”‚   β”‚   └── authStore.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”‚   └── auth.types.ts
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ posts/             # Posts feature
β”‚   β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ PostList.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ PostCard.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ PostForm.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ PostActions.tsx
β”‚   β”‚   β”‚   β”‚   └── PostSkeleton.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ usePosts.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useInfinitePosts.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useCreatePost.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useUpdatePost.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useDeletePost.ts
β”‚   β”‚   β”‚   β”‚   └── useLikePost.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”‚   └── postService.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”‚   └── post.types.ts
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ comments/          # Comments feature
β”‚   β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ CommentList.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ CommentItem.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ CommentForm.tsx
β”‚   β”‚   β”‚   β”‚   └── CommentReply.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useComments.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useAddComment.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useDeleteComment.ts
β”‚   β”‚   β”‚   β”‚   └── useLikeComment.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”‚   └── commentService.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”‚   └── comment.types.ts
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   β”œβ”€β”€ users/             # Users/profiles
β”‚   β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ UserProfile.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ UserCard.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ UserStats.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ FollowButton.tsx
β”‚   β”‚   β”‚   β”‚   └── EditProfileForm.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useUser.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useUserPosts.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ useFollowUser.ts
β”‚   β”‚   β”‚   β”‚   └── useUpdateProfile.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”‚   β”‚   └── userService.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”‚   └── user.types.ts
β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚
β”‚   β”‚   └── notifications/     # Notifications (optional)
β”‚   β”‚       β”œβ”€β”€ components/
β”‚   β”‚       β”œβ”€β”€ store/
β”‚   β”‚       β”œβ”€β”€ types/
β”‚   β”‚       └── index.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ shared/                # Shared across features
β”‚   β”‚   β”œβ”€β”€ components/        # Reusable UI
β”‚   β”‚   β”‚   β”œβ”€β”€ Button/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Button.tsx
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Button.module.css
β”‚   β”‚   β”‚   β”‚   └── Button.test.tsx
β”‚   β”‚   β”‚   β”œβ”€β”€ Input/
β”‚   β”‚   β”‚   β”œβ”€β”€ Card/
β”‚   β”‚   β”‚   β”œβ”€β”€ Modal/
β”‚   β”‚   β”‚   β”œβ”€β”€ Avatar/
β”‚   β”‚   β”‚   β”œβ”€β”€ Spinner/
β”‚   β”‚   β”‚   β”œβ”€β”€ ErrorBoundary/
β”‚   β”‚   β”‚   └── InfiniteScroll/
β”‚   β”‚   β”œβ”€β”€ hooks/             # Reusable hooks
β”‚   β”‚   β”‚   β”œβ”€β”€ useDebounce.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useLocalStorage.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ useMediaQuery.ts
β”‚   β”‚   β”‚   └── useIntersectionObserver.ts
β”‚   β”‚   β”œβ”€β”€ utils/             # Utility functions
β”‚   β”‚   β”‚   β”œβ”€β”€ formatDate.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ formatNumber.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ validators.ts
β”‚   β”‚   β”‚   └── helpers.ts
β”‚   β”‚   β”œβ”€β”€ types/             # Shared types
β”‚   β”‚   β”‚   β”œβ”€β”€ common.types.ts
β”‚   β”‚   β”‚   └── api.types.ts
β”‚   β”‚   β”œβ”€β”€ errors/            # Custom errors
β”‚   β”‚   β”‚   β”œβ”€β”€ AppError.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ ApiError.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ AuthError.ts
β”‚   β”‚   β”‚   └── ValidationError.ts
β”‚   β”‚   └── constants/
β”‚   β”‚       └── config.ts
β”‚   β”‚
β”‚   β”œβ”€β”€ lib/                   # Third-party setup
β”‚   β”‚   β”œβ”€β”€ axios.ts          # Axios instance
β”‚   β”‚   β”œβ”€β”€ react-query.ts    # React Query config
β”‚   β”‚   └── router.tsx         # Router config
β”‚   β”‚
β”‚   β”œβ”€β”€ pages/                 # Route components
β”‚   β”‚   β”œβ”€β”€ HomePage.tsx
β”‚   β”‚   β”œβ”€β”€ LoginPage.tsx
β”‚   β”‚   β”œβ”€β”€ SignupPage.tsx
β”‚   β”‚   β”œβ”€β”€ ProfilePage.tsx
β”‚   β”‚   β”œβ”€β”€ PostDetailPage.tsx
β”‚   β”‚   └── NotFoundPage.tsx
β”‚   β”‚
β”‚   β”œβ”€β”€ layouts/               # Layout components
β”‚   β”‚   β”œβ”€β”€ MainLayout.tsx
β”‚   β”‚   β”œβ”€β”€ AuthLayout.tsx
β”‚   β”‚   └── ProfileLayout.tsx
β”‚   β”‚
β”‚   β”œβ”€β”€ styles/                # Global styles
β”‚   β”‚   β”œβ”€β”€ globals.css
β”‚   β”‚   β”œβ”€β”€ variables.css
β”‚   β”‚   └── reset.css
β”‚   β”‚
β”‚   β”œβ”€β”€ App.tsx                # Root component
β”‚   β”œβ”€β”€ main.tsx               # Entry point
β”‚   └── vite-env.d.ts         # Vite types
β”‚
β”œβ”€β”€ .env.example               # Environment variables
β”œβ”€β”€ .gitignore
β”œβ”€β”€ index.html
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ vite.config.ts
└── README.md

βœ… Key Points About This Structure

  • Features are self-contained: Each feature has everything it needs
  • Shared folder for reusability: Common code goes here
  • Clear boundaries: Easy to see what belongs where
  • Scalable: Add new features without touching existing ones
  • Testable: Each piece can be tested independently
  • Type-safe: Types live close to where they're used

βš™οΈ Setup and Dependencies

Step 1: Create Project

# Create Vite project with React + TypeScript
npm create vite@latest social-media-feed -- --template react-ts

# Navigate to project
cd social-media-feed

# Install dependencies
npm install

Step 2: Install Required Dependencies

# State Management & Data Fetching
npm install @tanstack/react-query zustand
npm install axios

# Routing
npm install react-router-dom

# Forms & Validation
npm install react-hook-form @hookform/resolvers zod

# UI & Icons
npm install lucide-react
npm install react-hot-toast

# DevTools
npm install @tanstack/react-query-devtools --save-dev

Step 3: Install Type Definitions

# TypeScript types for libraries
npm install @types/react @types/react-dom --save-dev

Step 4: Project Configuration

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,

    /* Path aliases */
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/features/*": ["./src/features/*"],
      "@/shared/*": ["./src/shared/*"],
      "@/lib/*": ["./src/lib/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@/features': path.resolve(__dirname, './src/features'),
      '@/shared': path.resolve(__dirname, './src/shared'),
      '@/lib': path.resolve(__dirname, './src/lib'),
    },
  },
})

.env.example

# API Configuration
VITE_API_URL=http://localhost:3000/api
VITE_API_TIMEOUT=10000

# App Configuration
VITE_APP_NAME=Social Feed
VITE_APP_VERSION=1.0.0

⚠️ Important: Backend API

This project requires a backend API. You have three options:

  1. Use JSONPlaceholder: Free fake API (https://jsonplaceholder.typicode.com)
  2. Use Mock Service Worker (MSW): Mock API in browser
  3. Build your own: Create a simple Node.js/Express API

For learning purposes, we'll start with option 1 (JSONPlaceholder) and show you how to adapt it.

Step 5: Create Folder Structure

# Create all necessary folders
mkdir -p src/{features,shared,lib,pages,layouts,styles}
mkdir -p src/features/{auth,posts,comments,users}
mkdir -p src/shared/{components,hooks,utils,types,errors,constants}

# Create initial files
touch src/lib/{axios.ts,react-query.ts,router.tsx}
touch src/styles/{globals.css,variables.css}

βœ… Setup Complete!

You now have:

  • βœ… React + TypeScript project with Vite
  • βœ… All required dependencies installed
  • βœ… Path aliases configured
  • βœ… Feature-based folder structure
  • βœ… TypeScript strict mode enabled

Next: We'll start building the foundation with shared utilities and configurations.

πŸ—οΈ Phase 1: Foundation Setup

Before building features, we need to establish the foundation: shared utilities, error handling, API configuration, and base components. This infrastructure will be used throughout the application.

Step 1: Configure Axios Instance

// src/lib/axios.ts
import axios from 'axios';
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
const TIMEOUT = Number(import.meta.env.VITE_API_TIMEOUT) || 10000;

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

// Request interceptor - Add auth token
apiClient.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = localStorage.getItem('authToken');
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor - Handle errors globally
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    // Handle common errors
    if (error.response?.status === 401) {
      // Clear auth and redirect to login
      localStorage.removeItem('authToken');
      localStorage.removeItem('user');
      window.location.href = '/login';
    }

    return Promise.reject(error);
  }
);

Step 2: Create Custom Error Classes

// src/shared/errors/AppError.ts
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number,
    public details?: any
  ) {
    super(message);
    this.name = 'AppError';
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// src/shared/errors/ApiError.ts
import { AppError } from './AppError';

export class ApiError extends AppError {
  constructor(message: string, statusCode = 500, details?: any) {
    super(message, 'API_ERROR', statusCode, details);
    this.name = 'ApiError';
  }
}

// src/shared/errors/AuthError.ts
import { AppError } from './AppError';

export class AuthError extends AppError {
  constructor(message = 'Authentication required') {
    super(message, 'AUTH_ERROR', 401);
    this.name = 'AuthError';
  }
}

// src/shared/errors/ValidationError.ts
import { AppError } from './AppError';

export class ValidationError extends AppError {
  constructor(message: string, details?: Record<string, string>) {
    super(message, 'VALIDATION_ERROR', 400, details);
    this.name = 'ValidationError';
  }
}

// src/shared/errors/index.ts
export { AppError } from './AppError';
export { ApiError } from './ApiError';
export { AuthError } from './AuthError';
export { ValidationError } from './ValidationError';

Step 3: Configure React Query

// src/lib/react-query.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Global query defaults
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 10,   // 10 minutes (formerly cacheTime)
      refetchOnWindowFocus: false,
      refetchOnReconnect: true,
      retry: 1,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
    mutations: {
      // Global mutation defaults
      retry: 0,
      onError: (error) => {
        console.error('Mutation error:', error);
      },
    },
  },
});

Step 4: Shared TypeScript Types

// src/shared/types/common.types.ts
export type ID = string | number;

export interface ApiResponse<T> {
  data: T;
  message?: string;
  success: boolean;
}

export interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}

export interface ApiError {
  message: string;
  code: string;
  statusCode: number;
  details?: Record<string, any>;
}

export type Nullable<T> = T | null;
export type Optional<T> = T | undefined;

// src/shared/types/api.types.ts
export interface BaseEntity {
  id: string;
  createdAt: string;
  updatedAt: string;
}

export interface QueryParams {
  page?: number;
  limit?: number;
  sort?: string;
  order?: 'asc' | 'desc';
}

export interface InfiniteQueryParams extends QueryParams {
  cursor?: string;
}

Step 5: Utility Functions

// src/shared/utils/formatDate.ts
import { formatDistanceToNow, format } from 'date-fns';

export function formatRelativeTime(date: string | Date): string {
  return formatDistanceToNow(new Date(date), { addSuffix: true });
}

export function formatFullDate(date: string | Date): string {
  return format(new Date(date), 'PPP');
}

export function formatDateTime(date: string | Date): string {
  return format(new Date(date), 'PPP p');
}

// src/shared/utils/formatNumber.ts
export function formatNumber(num: number): string {
  if (num >= 1000000) {
    return `${(num / 1000000).toFixed(1)}M`;
  }
  if (num >= 1000) {
    return `${(num / 1000).toFixed(1)}K`;
  }
  return num.toString();
}

export function formatPlural(count: number, singular: string, plural?: string): string {
  if (count === 1) return `${count} ${singular}`;
  return `${count} ${plural || singular + 's'}`;
}

// src/shared/utils/validators.ts
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

export function isStrongPassword(password: string): boolean {
  // At least 8 chars, 1 uppercase, 1 lowercase, 1 number
  const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
  return passwordRegex.test(password);
}

export function truncateText(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength) + '...';
}

Step 6: Shared Custom Hooks

// src/shared/hooks/useDebounce.ts
import { useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

// src/shared/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// src/shared/hooks/useIntersectionObserver.ts
import { useEffect, useRef, useState } from 'react';

export function useIntersectionObserver(
  options?: IntersectionObserverInit
): [React.RefObject<HTMLDivElement>, boolean] {
  const ref = useRef<HTMLDivElement>(null);
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

    observer.observe(element);

    return () => observer.disconnect();
  }, [options]);

  return [ref, isIntersecting];
}

Step 7: Base UI Components

Button Component

// src/shared/components/Button/Button.tsx
import React from 'react';
import './Button.css';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
  size?: 'small' | 'medium' | 'large';
  loading?: boolean;
  children: React.ReactNode;
}

export function Button({
  variant = 'primary',
  size = 'medium',
  loading = false,
  disabled,
  children,
  className = '',
  ...props
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size} ${className}`}
      disabled={disabled || loading}
      {...props}
    >
      {loading ? (
        <span className="btn-spinner">Loading...</span>
      ) : (
        children
      )}
    </button>
  );
}

Avatar Component

// src/shared/components/Avatar/Avatar.tsx
import React from 'react';
import './Avatar.css';

interface AvatarProps {
  src?: string;
  alt: string;
  size?: 'small' | 'medium' | 'large';
  fallback?: string;
}

export function Avatar({ src, alt, size = 'medium', fallback }: AvatarProps) {
  const getInitials = (name: string): string => {
    return name
      .split(' ')
      .map(n => n[0])
      .join('')
      .toUpperCase()
      .slice(0, 2);
  };

  return (
    <div className={`avatar avatar-${size}`}>
      {src ? (
        <img src={src} alt={alt} className="avatar-img" />
      ) : (
        <div className="avatar-fallback">
          {fallback || getInitials(alt)}
        </div>
      )}
    </div>
  );
}

Card Component

// src/shared/components/Card/Card.tsx
import React from 'react';
import './Card.css';

interface CardProps {
  children: React.ReactNode;
  className?: string;
  padding?: 'small' | 'medium' | 'large';
  hoverable?: boolean;
}

export function Card({ 
  children, 
  className = '', 
  padding = 'medium',
  hoverable = false 
}: CardProps) {
  return (
    <div 
      className={`card card-padding-${padding} ${hoverable ? 'card-hoverable' : ''} ${className}`}
    >
      {children}
    </div>
  );
}

Spinner Component

// src/shared/components/Spinner/Spinner.tsx
import React from 'react';
import './Spinner.css';

interface SpinnerProps {
  size?: 'small' | 'medium' | 'large';
  text?: string;
}

export function Spinner({ size = 'medium', text }: SpinnerProps) {
  return (
    <div className="spinner-container">
      <div className={`spinner spinner-${size}`}></div>
      {text && <p className="spinner-text">{text}</p>}
    </div>
  );
}

Error Boundary

// src/shared/components/ErrorBoundary/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: (error: Error, reset: () => void) => ReactNode;
}

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

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught an error:', error, errorInfo);
  }

  reset = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError && this.state.error) {
      if (this.props.fallback) {
        return this.props.fallback(this.state.error, this.reset);
      }

      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>{this.state.error.message}</p>
          <button onClick={this.reset}>Try again</button>
        </div>
      );
    }

    return this.props.children;
  }
}

βœ… Phase 1 Complete!

You now have:

  • βœ… Axios configured with interceptors
  • βœ… Custom error classes
  • βœ… React Query configured
  • βœ… Shared TypeScript types
  • βœ… Utility functions
  • βœ… Custom hooks (debounce, localStorage, intersection observer)
  • βœ… Base UI components (Button, Avatar, Card, Spinner, ErrorBoundary)

Next: We'll build the Posts feature with full CRUD operations.

πŸ“ Phase 2: Posts Feature

The Posts feature is the core of our social media feed. We'll implement infinite scrolling, CRUD operations, and optimistic updates for likes.

Step 1: Define Post Types

// src/features/posts/types/post.types.ts
import type { BaseEntity } from '@/shared/types/api.types';

export interface Post extends BaseEntity {
  content: string;
  authorId: string;
  author: {
    id: string;
    name: string;
    username: string;
    avatar?: string;
  };
  likes: number;
  likedBy: string[];
  commentsCount: number;
  sharesCount: number;
  isLiked?: boolean; // Computed based on current user
}

export interface CreatePostDto {
  content: string;
}

export interface UpdatePostDto {
  content: string;
}

export interface PostFilters {
  authorId?: string;
  search?: string;
}

export interface InfinitePostsResponse {
  posts: Post[];
  nextCursor: string | null;
  hasMore: boolean;
}

Step 2: Create Post Service

// src/features/posts/api/postService.ts
import { apiClient } from '@/lib/axios';
import type { 
  Post, 
  CreatePostDto, 
  UpdatePostDto, 
  InfinitePostsResponse 
} from '../types/post.types';

export const postService = {
  /**
   * Get paginated posts (infinite scroll)
   */
  async getInfinitePosts(cursor?: string, limit = 10): Promise<InfinitePostsResponse> {
    const params = new URLSearchParams();
    if (cursor) params.append('cursor', cursor);
    params.append('limit', limit.toString());

    const response = await apiClient.get<InfinitePostsResponse>(
      `/posts?${params.toString()}`
    );
    return response.data;
  },

  /**
   * Get single post by ID
   */
  async getById(id: string): Promise<Post> {
    const response = await apiClient.get<Post>(`/posts/${id}`);
    return response.data;
  },

  /**
   * Get posts by user
   */
  async getByUser(userId: string): Promise<Post[]> {
    const response = await apiClient.get<Post[]>(`/users/${userId}/posts`);
    return response.data;
  },

  /**
   * Create new post
   */
  async create(data: CreatePostDto): Promise<Post> {
    const response = await apiClient.post<Post>('/posts', data);
    return response.data;
  },

  /**
   * Update post
   */
  async update(id: string, data: UpdatePostDto): Promise<Post> {
    const response = await apiClient.patch<Post>(`/posts/${id}`, data);
    return response.data;
  },

  /**
   * Delete post
   */
  async delete(id: string): Promise<void> {
    await apiClient.delete(`/posts/${id}`);
  },

  /**
   * Like/unlike post
   */
  async toggleLike(id: string): Promise<Post> {
    const response = await apiClient.post<Post>(`/posts/${id}/like`);
    return response.data;
  },
};

Step 3: Create Custom Hooks

Query Keys Factory

// src/features/posts/hooks/postKeys.ts
export const postKeys = {
  all: ['posts'] as const,
  lists: () => [...postKeys.all, 'list'] as const,
  list: (filters: string) => [...postKeys.lists(), { filters }] as const,
  infinite: () => [...postKeys.all, 'infinite'] as const,
  details: () => [...postKeys.all, 'detail'] as const,
  detail: (id: string) => [...postKeys.details(), id] as const,
  byUser: (userId: string) => [...postKeys.all, 'user', userId] as const,
};

useInfinitePosts Hook

// src/features/posts/hooks/useInfinitePosts.ts
import { useInfiniteQuery } from '@tanstack/react-query';
import { postService } from '../api/postService';
import { postKeys } from './postKeys';

export function useInfinitePosts() {
  return useInfiniteQuery({
    queryKey: postKeys.infinite(),
    queryFn: ({ pageParam }) => postService.getInfinitePosts(pageParam),
    initialPageParam: undefined as string | undefined,
    getNextPageParam: (lastPage) => {
      return lastPage.hasMore ? lastPage.nextCursor : undefined;
    },
    staleTime: 1000 * 60 * 2, // 2 minutes
  });
}

usePost Hook

// src/features/posts/hooks/usePost.ts
import { useQuery } from '@tanstack/react-query';
import { postService } from '../api/postService';
import { postKeys } from './postKeys';

export function usePost(id: string) {
  return useQuery({
    queryKey: postKeys.detail(id),
    queryFn: () => postService.getById(id),
    staleTime: 1000 * 60 * 5, // 5 minutes
  });
}

useCreatePost Hook

// src/features/posts/hooks/useCreatePost.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { postService } from '../api/postService';
import { postKeys } from './postKeys';
import type { CreatePostDto, Post } from '../types/post.types';
import toast from 'react-hot-toast';

export function useCreatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreatePostDto) => postService.create(data),
    onSuccess: (newPost: Post) => {
      // Invalidate infinite query to show new post
      queryClient.invalidateQueries({ queryKey: postKeys.infinite() });
      queryClient.invalidateQueries({ queryKey: postKeys.lists() });
      
      toast.success('Post created successfully!');
    },
    onError: (error: Error) => {
      toast.error(error.message || 'Failed to create post');
    },
  });
}

useUpdatePost Hook

// src/features/posts/hooks/useUpdatePost.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { postService } from '../api/postService';
import { postKeys } from './postKeys';
import type { UpdatePostDto } from '../types/post.types';
import toast from 'react-hot-toast';

export function useUpdatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdatePostDto }) =>
      postService.update(id, data),
    onSuccess: (updatedPost) => {
      // Update specific post cache
      queryClient.setQueryData(postKeys.detail(updatedPost.id), updatedPost);
      
      // Invalidate lists
      queryClient.invalidateQueries({ queryKey: postKeys.infinite() });
      queryClient.invalidateQueries({ queryKey: postKeys.lists() });
      
      toast.success('Post updated successfully!');
    },
    onError: (error: Error) => {
      toast.error(error.message || 'Failed to update post');
    },
  });
}

useDeletePost Hook

// src/features/posts/hooks/useDeletePost.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { postService } from '../api/postService';
import { postKeys } from './postKeys';
import toast from 'react-hot-toast';

export function useDeletePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) => postService.delete(postId),
    onSuccess: (_, deletedId) => {
      // Remove from cache
      queryClient.removeQueries({ queryKey: postKeys.detail(deletedId) });
      
      // Invalidate lists
      queryClient.invalidateQueries({ queryKey: postKeys.infinite() });
      queryClient.invalidateQueries({ queryKey: postKeys.lists() });
      
      toast.success('Post deleted successfully!');
    },
    onError: (error: Error) => {
      toast.error(error.message || 'Failed to delete post');
    },
  });
}

useLikePost Hook (with Optimistic Updates)

// src/features/posts/hooks/useLikePost.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { postService } from '../api/postService';
import { postKeys } from './postKeys';
import type { Post } from '../types/post.types';

export function useLikePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) => postService.toggleLike(postId),
    
    // Optimistic update
    onMutate: async (postId) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: postKeys.detail(postId) });
      
      // Snapshot previous value
      const previousPost = queryClient.getQueryData<Post>(
        postKeys.detail(postId)
      );
      
      // Optimistically update cache
      queryClient.setQueryData<Post>(postKeys.detail(postId), (old) => {
        if (!old) return old;
        
        const isCurrentlyLiked = old.isLiked;
        
        return {
          ...old,
          likes: isCurrentlyLiked ? old.likes - 1 : old.likes + 1,
          isLiked: !isCurrentlyLiked,
        };
      });
      
      return { previousPost };
    },
    
    // Rollback on error
    onError: (error, postId, context) => {
      if (context?.previousPost) {
        queryClient.setQueryData(postKeys.detail(postId), context.previousPost);
      }
    },
    
    // Always refetch after success or error
    onSettled: (data, error, postId) => {
      queryClient.invalidateQueries({ queryKey: postKeys.detail(postId) });
      queryClient.invalidateQueries({ queryKey: postKeys.infinite() });
    },
  });
}

Step 4: Export Public API

// src/features/posts/index.ts
// Components
export { PostList } from './components/PostList';
export { PostCard } from './components/PostCard';
export { PostForm } from './components/PostForm';

// Hooks
export { useInfinitePosts } from './hooks/useInfinitePosts';
export { usePost } from './hooks/usePost';
export { useCreatePost } from './hooks/useCreatePost';
export { useUpdatePost } from './hooks/useUpdatePost';
export { useDeletePost } from './hooks/useDeletePost';
export { useLikePost } from './hooks/useLikePost';

// Types
export type { Post, CreatePostDto, UpdatePostDto } from './types/post.types';

βœ… Phase 2 Foundation Complete!

You now have the complete Posts feature infrastructure:

  • βœ… TypeScript types defined
  • βœ… Service layer with all CRUD operations
  • βœ… Query keys factory for consistent caching
  • βœ… Custom hooks for all operations
  • βœ… Optimistic updates for instant feedback
  • βœ… Error handling with toast notifications
  • βœ… Public API exported

Next: We'll create the UI components (PostList, PostCard, PostForm) to display and interact with posts.

🎨 Phase 3: Posts UI Components

Now that we have the data layer complete, let's build the user interface components for displaying and interacting with posts.

PostCard Component

// src/features/posts/components/PostCard.tsx
import { Heart, MessageCircle, Trash2, Edit2 } from 'lucide-react';
import { Avatar } from '@/shared/components/Avatar';
import { Card } from '@/shared/components/Card';
import { Button } from '@/shared/components/Button';
import { formatRelativeTime } from '@/shared/utils/formatDate';
import { formatNumber } from '@/shared/utils/formatNumber';
import { useLikePost, useDeletePost } from '../hooks';
import type { Post } from '../types/post.types';

interface PostCardProps {
  post: Post;
  onEdit?: (post: Post) => void;
  onCommentClick?: (post: Post) => void;
}

export function PostCard({ post, onEdit, onCommentClick }: PostCardProps) {
  const likeMutation = useLikePost();
  const deleteMutation = useDeletePost();
  const currentUserId = 'current-user-id'; // Get from auth store

  const handleLike = () => {
    likeMutation.mutate(post.id);
  };

  const handleDelete = () => {
    if (window.confirm('Are you sure you want to delete this post?')) {
      deleteMutation.mutate(post.id);
    }
  };

  const isOwnPost = post.authorId === currentUserId;

  return (
    <Card className="post-card">
      {/* Header */}
      <div className="post-header">
        <Avatar
          src={post.author.avatar}
          alt={post.author.name}
          size="medium"
        />
        <div className="post-author-info">
          <h4>{post.author.name}</h4>
          <p className="post-username">@{post.author.username}</p>
        </div>
        <span className="post-time">
          {formatRelativeTime(post.createdAt)}
        </span>
      </div>

      {/* Content */}
      <div className="post-content">
        <p>{post.content}</p>
      </div>

      {/* Actions */}
      <div className="post-actions">
        <button
          className={`post-action-btn ${post.isLiked ? 'liked' : ''}`}
          onClick={handleLike}
          disabled={likeMutation.isPending}
        >
          <Heart fill={post.isLiked ? 'currentColor' : 'none'} />
          <span>{formatNumber(post.likes)}</span>
        </button>

        <button
          className="post-action-btn"
          onClick={() => onCommentClick?.(post)}
        >
          <MessageCircle />
          <span>{formatNumber(post.commentsCount)}</span>
        </button>

        {isOwnPost && (
          <>
            <button
              className="post-action-btn"
              onClick={() => onEdit?.(post)}
            >
              <Edit2 size={18} />
            </button>
            <button
              className="post-action-btn danger"
              onClick={handleDelete}
              disabled={deleteMutation.isPending}
            >
              <Trash2 size={18} />
            </button>
          </>
        )}
      </div>
    </Card>
  );
}

PostList Component with Infinite Scroll

// src/features/posts/components/PostList.tsx
import { useEffect } from 'react';
import { Spinner } from '@/shared/components/Spinner';
import { useIntersectionObserver } from '@/shared/hooks/useIntersectionObserver';
import { useInfinitePosts } from '../hooks';
import { PostCard } from './PostCard';

export function PostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    isError,
    error,
  } = useInfinitePosts();

  // Intersection observer for infinite scroll
  const [loadMoreRef, isIntersecting] = useIntersectionObserver({
    threshold: 0.5,
  });

  useEffect(() => {
    if (isIntersecting && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  }, [isIntersecting, hasNextPage, isFetchingNextPage, fetchNextPage]);

  if (isLoading) {
    return <Spinner size="large" text="Loading posts..." />;
  }

  if (isError) {
    return (
      <div className="error-message">
        <p>Error loading posts: {error.message}</p>
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  const posts = data?.pages.flatMap((page) => page.posts) ?? [];

  if (posts.length === 0) {
    return (
      <div className="empty-state">
        <p>No posts yet. Be the first to post!</p>
      </div>
    );
  }

  return (
    <div className="post-list">
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}

      {/* Load more trigger */}
      <div ref={loadMoreRef} className="load-more-trigger">
        {isFetchingNextPage && <Spinner text="Loading more..." />}
      </div>

      {!hasNextPage && posts.length > 0 && (
        <p className="end-message">You've reached the end!</p>
      )}
    </div>
  );
}

PostForm Component

// src/features/posts/components/PostForm.tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/shared/components/Button';
import { useCreatePost } from '../hooks';
import type { CreatePostDto } from '../types/post.types';

const postSchema = z.object({
  content: z
    .string()
    .min(1, 'Post cannot be empty')
    .max(280, 'Post cannot exceed 280 characters'),
});

type PostFormData = z.infer<typeof postSchema>;

export function PostForm() {
  const createMutation = useCreatePost();
  const [charCount, setCharCount] = useState(0);

  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
    watch,
  } = useForm<PostFormData>({
    resolver: zodResolver(postSchema),
  });

  // Watch content for character count
  const content = watch('content', '');
  useEffect(() => {
    setCharCount(content.length);
  }, [content]);

  const onSubmit = async (data: PostFormData) => {
    await createMutation.mutateAsync(data);
    reset();
    setCharCount(0);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="post-form">
      <textarea
        {...register('content')}
        placeholder="What's on your mind?"
        className="post-textarea"
        rows={3}
      />

      {errors.content && (
        <p className="error-text">{errors.content.message}</p>
      )}

      <div className="post-form-footer">
        <span className={`char-count ${charCount > 280 ? 'error' : ''}`}>
          {charCount}/280
        </span>
        <Button
          type="submit"
          loading={createMutation.isPending}
          disabled={createMutation.isPending}
        >
          Post
        </Button>
      </div>
    </form>
  );
}

βœ… Posts Feature Complete!

You now have a fully functional Posts feature with:

  • βœ… PostCard component with like, comment, edit, delete actions
  • βœ… PostList with infinite scroll
  • βœ… PostForm with character counter and validation
  • βœ… Optimistic UI updates
  • βœ… Loading and error states
  • βœ… Complete TypeScript coverage

πŸ’¬ Phase 4: Comments Feature

The Comments feature follows the same architectural pattern as Posts. We'll create types, services, hooks, and components.

Step 1: Define Comment Types

// src/features/comments/types/comment.types.ts
import type { BaseEntity } from '@/shared/types/api.types';

export interface Comment extends BaseEntity {
  content: string;
  postId: string;
  authorId: string;
  author: {
    id: string;
    name: string;
    username: string;
    avatar?: string;
  };
  likes: number;
  isLiked?: boolean;
  parentId?: string; // For nested replies
  replies?: Comment[];
}

export interface CreateCommentDto {
  content: string;
  postId: string;
  parentId?: string;
}

export interface UpdateCommentDto {
  content: string;
}

Step 2: Create Comment Service

// src/features/comments/api/commentService.ts
import { apiClient } from '@/lib/axios';
import type { Comment, CreateCommentDto, UpdateCommentDto } from '../types/comment.types';

export const commentService = {
  async getByPost(postId: string): Promise<Comment[]> {
    const response = await apiClient.get<Comment[]>(`/posts/${postId}/comments`);
    return response.data;
  },

  async create(data: CreateCommentDto): Promise<Comment> {
    const response = await apiClient.post<Comment>('/comments', data);
    return response.data;
  },

  async update(id: string, data: UpdateCommentDto): Promise<Comment> {
    const response = await apiClient.patch<Comment>(`/comments/${id}`, data);
    return response.data;
  },

  async delete(id: string): Promise<void> {
    await apiClient.delete(`/comments/${id}`);
  },

  async toggleLike(id: string): Promise<Comment> {
    const response = await apiClient.post<Comment>(`/comments/${id}/like`);
    return response.data;
  },
};

Step 3: Create Comment Hooks

// src/features/comments/hooks/useComments.ts
import { useQuery } from '@tanstack/react-query';
import { commentService } from '../api/commentService';

export const commentKeys = {
  all: ['comments'] as const,
  byPost: (postId: string) => [...commentKeys.all, 'post', postId] as const,
};

export function useComments(postId: string) {
  return useQuery({
    queryKey: commentKeys.byPost(postId),
    queryFn: () => commentService.getByPost(postId),
    staleTime: 1000 * 60 * 2,
  });
}

// src/features/comments/hooks/useAddComment.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { commentService } from '../api/commentService';
import { commentKeys } from './useComments';
import { postKeys } from '@/features/posts/hooks/postKeys';
import toast from 'react-hot-toast';

export function useAddComment() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: commentService.create,
    onSuccess: (newComment) => {
      // Invalidate comments for this post
      queryClient.invalidateQueries({ 
        queryKey: commentKeys.byPost(newComment.postId) 
      });
      
      // Update post's comment count
      queryClient.invalidateQueries({ 
        queryKey: postKeys.detail(newComment.postId) 
      });
      
      toast.success('Comment added!');
    },
    onError: (error: Error) => {
      toast.error(error.message || 'Failed to add comment');
    },
  });
}

Step 4: CommentList Component

// src/features/comments/components/CommentList.tsx
import { useState } from 'react';
import { Spinner } from '@/shared/components/Spinner';
import { useComments } from '../hooks/useComments';
import { CommentItem } from './CommentItem';
import { CommentForm } from './CommentForm';

interface CommentListProps {
  postId: string;
}

export function CommentList({ postId }: CommentListProps) {
  const { data: comments, isLoading, isError } = useComments(postId);
  const [replyingTo, setReplyingTo] = useState<string | null>(null);

  if (isLoading) {
    return <Spinner text="Loading comments..." />;
  }

  if (isError) {
    return <p>Failed to load comments</p>;
  }

  return (
    <div className="comments-section">
      <h3>Comments ({comments?.length || 0})</h3>
      
      <CommentForm postId={postId} />

      <div className="comments-list">
        {comments?.map((comment) => (
          <CommentItem
            key={comment.id}
            comment={comment}
            onReply={() => setReplyingTo(comment.id)}
          />
        ))}
      </div>
    </div>
  );
}

βœ… Comments Feature Complete!

Following the same pattern as Posts, you now have:

  • βœ… Comment types with nested replies support
  • βœ… Comment service for API calls
  • βœ… Custom hooks with cache management
  • βœ… Comment UI components

πŸ‘€ Phase 5: Authentication & Users

Auth Store with Zustand

// src/features/auth/store/authStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: string;
  name: string;
  username: string;
  email: string;
  avatar?: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (user: User, token: string) => void;
  logout: () => void;
  updateUser: (user: Partial<User>) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: (user, token) => {
        localStorage.setItem('authToken', token);
        set({ user, token, isAuthenticated: true });
      },

      logout: () => {
        localStorage.removeItem('authToken');
        set({ user: null, token: null, isAuthenticated: false });
      },

      updateUser: (userData) => {
        set((state) => ({
          user: state.user ? { ...state.user, ...userData } : null,
        }));
      },
    }),
    {
      name: 'auth-storage',
    }
  )
);

Protected Route Component

// src/features/auth/components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';

interface ProtectedRouteProps {
  children: React.ReactNode;
}

export function ProtectedRoute({ children }: ProtectedRouteProps) {
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <>{children}</>;
}

Main App Structure

// src/App.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { BrowserRouter } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import { queryClient } from './lib/react-query';
import { AppRoutes } from './lib/router';
import { ErrorBoundary } from './shared/components/ErrorBoundary';

export function App() {
  return (
    <ErrorBoundary>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <AppRoutes />
          <Toaster position="top-right" />
        </BrowserRouter>
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </ErrorBoundary>
  );
}

πŸŽ‰ Congratulations!

You've built a complete, production-ready social media feed application with:

  • βœ… Feature-based architecture
  • βœ… React Query for server state
  • βœ… Zustand for client state
  • βœ… Service layer pattern
  • βœ… Custom hooks for business logic
  • βœ… Optimistic updates
  • βœ… Infinite scroll
  • βœ… Complete TypeScript coverage
  • βœ… Error handling at every layer
  • βœ… Professional code organization

πŸ‹οΈ Extension Exercises

Exercise 1: Add Search Functionality

Goal: Implement post and user search with debouncing.

Tasks:

  1. Create a search input component
  2. Use useDebounce hook for the search term
  3. Add search endpoint to post service
  4. Create useSearchPosts hook
  5. Display search results with highlighting

Exercise 2: Add User Profiles

Goal: Create user profile pages with their posts and follower stats.

Tasks:

  1. Create user types and service
  2. Build UserProfile component
  3. Implement follow/unfollow functionality
  4. Show user's posts on their profile
  5. Display follower/following counts

Exercise 3: Add Notifications

Goal: Implement a notifications system for likes and comments.

Tasks:

  1. Create notifications feature module
  2. Build Zustand store for notifications
  3. Add NotificationBell component
  4. Show unread count badge
  5. Mark notifications as read

πŸ“ Project Summary

πŸŽ‰ Module 8 Project Complete!

Congratulations on building a production-ready social media feed application! This project demonstrates every architectural pattern and state management technique you learned in Module 8.

What You Built

  • βœ… Feature-Based Architecture: Scalable folder structure by feature
  • βœ… React Query Integration: Server state with caching and optimistic updates
  • βœ… Zustand State Management: Client state for auth and UI
  • βœ… Service Layer: Clean API abstraction with axios
  • βœ… Custom Hooks: Reusable business logic
  • βœ… Component Patterns: Container and Presentational separation
  • βœ… Infinite Scroll: Performant feed with cursor pagination
  • βœ… Error Handling: Multi-layer error architecture
  • βœ… TypeScript: Full type safety throughout
  • βœ… Optimistic UI: Instant feedback for user actions

🎯 Key Achievements

  1. Professional Architecture: Your code is organized like production applications at top companies
  2. Scalable Patterns: The patterns you used scale from small projects to enterprise apps
  3. Best Practices: Separation of concerns, DRY, single responsibility throughout
  4. Modern Stack: React Query, Zustand, TypeScript - industry standard tools
  5. Performance: Optimistic updates, infinite scroll, proper caching
  6. Maintainability: Easy to find, modify, and extend features

πŸš€ What's Next?

You've completed Module 8! Here's what comes next:

Immediate Next Steps:

  • Complete the extension exercises to deepen your understanding
  • Deploy your social media feed to Vercel or Netlify
  • Add it to your portfolio with screenshots and live demo
  • Consider building a real backend API for it

Module 9: Testing React Applications

In the next module, you'll learn how to:

  • Write comprehensive tests for React applications
  • Use React Testing Library
  • Test hooks, components, and user interactions
  • Mock API calls and test async code
  • Write integration and E2E tests

πŸ“š Additional Challenges

To further improve your skills, try these challenges:

  1. Image Upload: Add image support for posts using file upload
  2. Real-Time Updates: Implement WebSocket for live notifications
  3. Dark Mode: Add theme switching with Zustand
  4. Hashtags: Implement hashtag parsing and trending topics
  5. Advanced Search: Add filters (date range, author, hashtags)
  6. Analytics: Track user engagement and post performance
  7. Accessibility: Ensure full keyboard navigation and ARIA labels
  8. Tests: Add comprehensive test coverage (Module 9 skill!)

πŸ’Ό Portfolio Presentation

This project is portfolio-ready! When presenting it:

  • Highlight the architecture: Explain your feature-based structure
  • Discuss state management decisions: Why React Query vs Zustand for different state types
  • Show the code organization: Service layer, custom hooks, component separation
  • Demonstrate optimistic updates: The instant like feedback
  • Explain scalability: How easy it is to add new features
  • Mention TypeScript: Type safety throughout the application