π― 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.
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
anytypes - 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
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
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:
- Use JSONPlaceholder: Free fake API (https://jsonplaceholder.typicode.com)
- Use Mock Service Worker (MSW): Mock API in browser
- 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:
- Create a search input component
- Use
useDebouncehook for the search term - Add search endpoint to post service
- Create
useSearchPostshook - Display search results with highlighting
Exercise 2: Add User Profiles
Goal: Create user profile pages with their posts and follower stats.
Tasks:
- Create user types and service
- Build UserProfile component
- Implement follow/unfollow functionality
- Show user's posts on their profile
- Display follower/following counts
Exercise 3: Add Notifications
Goal: Implement a notifications system for likes and comments.
Tasks:
- Create notifications feature module
- Build Zustand store for notifications
- Add NotificationBell component
- Show unread count badge
- 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
- Professional Architecture: Your code is organized like production applications at top companies
- Scalable Patterns: The patterns you used scale from small projects to enterprise apps
- Best Practices: Separation of concerns, DRY, single responsibility throughout
- Modern Stack: React Query, Zustand, TypeScript - industry standard tools
- Performance: Optimistic updates, infinite scroll, proper caching
- 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:
- Image Upload: Add image support for posts using file upload
- Real-Time Updates: Implement WebSocket for live notifications
- Dark Mode: Add theme switching with Zustand
- Hashtags: Implement hashtag parsing and trending topics
- Advanced Search: Add filters (date range, author, hashtags)
- Analytics: Track user engagement and post performance
- Accessibility: Ensure full keyboard navigation and ARIA labels
- 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