React.js Fundamentals

Building a Complete React Project

20 min Lesson 39 of 40

Project Overview: Task Manager Application

In this lesson, we'll build a complete, production-ready Task Manager application that demonstrates all the concepts we've learned. This app will include CRUD operations, routing, state management, API integration, authentication, and modern React patterns.

Features We'll Build

Application Features:
  • User authentication (login/register/logout)
  • Create, read, update, delete tasks
  • Filter tasks by status (all, active, completed)
  • Search tasks by title
  • Mark tasks as complete/incomplete
  • Persistent data with API
  • Protected routes
  • Loading states and error handling
  • Responsive design

Project Structure

task-manager/ ├── public/ ├── src/ │ ├── components/ │ │ ├── common/ │ │ │ ├── Button.jsx │ │ │ ├── Input.jsx │ │ │ └── Spinner.jsx │ │ ├── layout/ │ │ │ ├── Header.jsx │ │ │ └── Layout.jsx │ │ └── tasks/ │ │ ├── TaskList.jsx │ │ ├── TaskItem.jsx │ │ ├── TaskForm.jsx │ │ └── TaskFilters.jsx │ ├── features/ │ │ └── auth/ │ │ ├── Login.jsx │ │ ├── Register.jsx │ │ └── useAuth.js │ ├── hooks/ │ │ ├── useLocalStorage.js │ │ └── useTasks.js │ ├── services/ │ │ ├── api.js │ │ └── taskService.js │ ├── context/ │ │ ├── AuthContext.jsx │ │ └── TaskContext.jsx │ ├── routes/ │ │ ├── ProtectedRoute.jsx │ │ └── index.jsx │ ├── App.jsx │ └── index.jsx └── package.json

Step 1: Setting Up the Project

# Create project with Vite npm create vite@latest task-manager -- --template react cd task-manager npm install # Install dependencies npm install react-router-dom axios npm install -D tailwindcss postcss autoprefixer # Initialize Tailwind npx tailwindcss init -p

Step 2: API Service Setup

// src/services/api.js import axios from 'axios'; const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // Add auth token to requests api.interceptors.request.use((config) => { const token = localStorage.getItem('authToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Handle token expiration api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { localStorage.removeItem('authToken'); window.location.href = '/login'; } return Promise.reject(error); } ); export default api;
// src/services/taskService.js import api from './api'; export const taskService = { // Get all tasks getAllTasks: async () => { const response = await api.get('/tasks'); return response.data; }, // Get task by ID getTaskById: async (id) => { const response = await api.get(`/tasks/${id}`); return response.data; }, // Create new task createTask: async (taskData) => { const response = await api.post('/tasks', taskData); return response.data; }, // Update task updateTask: async (id, taskData) => { const response = await api.put(`/tasks/${id}`, taskData); return response.data; }, // Delete task deleteTask: async (id) => { const response = await api.delete(`/tasks/${id}`); return response.data; }, // Toggle task completion toggleTask: async (id) => { const response = await api.patch(`/tasks/${id}/toggle`); return response.data; }, };

Step 3: Authentication Context

// src/context/AuthContext.jsx import { createContext, useContext, useState, useEffect } from 'react'; import api from '../services/api'; const AuthContext = createContext(null); export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // Check if user is logged in const token = localStorage.getItem('authToken'); if (token) { fetchUser(); } else { setLoading(false); } }, []); const fetchUser = async () => { try { const response = await api.get('/auth/me'); setUser(response.data); } catch (error) { localStorage.removeItem('authToken'); } finally { setLoading(false); } }; const login = async (email, password) => { const response = await api.post('/auth/login', { email, password }); const { token, user } = response.data; localStorage.setItem('authToken', token); setUser(user); return user; }; const register = async (name, email, password) => { const response = await api.post('/auth/register', { name, email, password, }); const { token, user } = response.data; localStorage.setItem('authToken', token); setUser(user); return user; }; const logout = () => { localStorage.removeItem('authToken'); setUser(null); }; const value = { user, loading, login, register, logout, isAuthenticated: !!user, }; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; } export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within AuthProvider'); } return context; };

Step 4: Task Context with CRUD Operations

// src/context/TaskContext.jsx import { createContext, useContext, useState, useEffect } from 'react'; import { taskService } from '../services/taskService'; import { useAuth } from './AuthContext'; const TaskContext = createContext(null); export function TaskProvider({ children }) { const { isAuthenticated } = useAuth(); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [filter, setFilter] = useState('all'); // all, active, completed const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { if (isAuthenticated) { loadTasks(); } }, [isAuthenticated]); const loadTasks = async () => { setLoading(true); setError(null); try { const data = await taskService.getAllTasks(); setTasks(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const addTask = async (taskData) => { try { const newTask = await taskService.createTask(taskData); setTasks((prev) => [newTask, ...prev]); return newTask; } catch (err) { setError(err.message); throw err; } }; const updateTask = async (id, taskData) => { try { const updated = await taskService.updateTask(id, taskData); setTasks((prev) => prev.map((task) => (task.id === id ? updated : task))); return updated; } catch (err) { setError(err.message); throw err; } }; const deleteTask = async (id) => { try { await taskService.deleteTask(id); setTasks((prev) => prev.filter((task) => task.id !== id)); } catch (err) { setError(err.message); throw err; } }; const toggleTask = async (id) => { try { const updated = await taskService.toggleTask(id); setTasks((prev) => prev.map((task) => (task.id === id ? updated : task))); } catch (err) { setError(err.message); throw err; } }; // Filtered tasks based on filter and search const filteredTasks = tasks .filter((task) => { if (filter === 'active') return !task.completed; if (filter === 'completed') return task.completed; return true; }) .filter((task) => task.title.toLowerCase().includes(searchQuery.toLowerCase()) ); const value = { tasks: filteredTasks, loading, error, filter, setFilter, searchQuery, setSearchQuery, addTask, updateTask, deleteTask, toggleTask, refreshTasks: loadTasks, }; return <TaskContext.Provider value={value}>{children}</TaskContext.Provider>; } export const useTasks = () => { const context = useContext(TaskContext); if (!context) { throw new Error('useTasks must be used within TaskProvider'); } return context; };

Step 5: Protected Route Component

// src/routes/ProtectedRoute.jsx import { Navigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; import Spinner from '../components/common/Spinner'; export default function ProtectedRoute({ children }) { const { isAuthenticated, loading } = useAuth(); if (loading) { return ( <div className="flex items-center justify-center min-h-screen"> <Spinner /> </div> ); } if (!isAuthenticated) { return <Navigate to="/login" replace />; } return children; }

Step 6: Task Components

// src/components/tasks/TaskForm.jsx import { useState } from 'react'; import { useTasks } from '../../context/TaskContext'; import Button from '../common/Button'; import Input from '../common/Input'; export default function TaskForm() { const { addTask } = useTasks(); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); if (!title.trim()) return; setLoading(true); try { await addTask({ title: title.trim(), description: description.trim(), completed: false, }); setTitle(''); setDescription(''); } catch (error) { console.error('Failed to add task:', error); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit} className="space-y-4 mb-6"> <Input type="text" placeholder="Task title..." value={title} onChange={(e) => setTitle(e.target.value)} disabled={loading} /> <textarea className="w-full px-4 py-2 border rounded-lg" placeholder="Description (optional)..." value={description} onChange={(e) => setDescription(e.target.value)} disabled={loading} rows={3} /> <Button type="submit" disabled={loading || !title.trim()}> {loading ? 'Adding...' : 'Add Task'} </Button> </form> ); }
// src/components/tasks/TaskItem.jsx import { useState } from 'react'; import { useTasks } from '../../context/TaskContext'; import Button from '../common/Button'; export default function TaskItem({ task }) { const { updateTask, deleteTask, toggleTask } = useTasks(); const [isEditing, setIsEditing] = useState(false); const [title, setTitle] = useState(task.title); const handleUpdate = async () => { if (title.trim() && title !== task.title) { await updateTask(task.id, { ...task, title: title.trim() }); } setIsEditing(false); }; const handleDelete = async () => { if (confirm('Are you sure you want to delete this task?')) { await deleteTask(task.id); } }; return ( <div className="flex items-center gap-3 p-4 bg-white rounded-lg shadow"> <input type="checkbox" checked={task.completed} onChange={() => toggleTask(task.id)} className="w-5 h-5" /> {isEditing ? ( <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} onBlur={handleUpdate} onKeyPress={(e) => e.key === 'Enter' && handleUpdate()} className="flex-1 px-2 py-1 border rounded" autoFocus /> ) : ( <div className="flex-1"> <h3 className={`font-medium ${ task.completed ? 'line-through text-gray-500' : '' }`} > {task.title} </h3> {task.description && ( <p className="text-sm text-gray-600">{task.description}</p> )} </div> )} <div className="flex gap-2"> <Button variant="secondary" onClick={() => setIsEditing(!isEditing)}> {isEditing ? 'Cancel' : 'Edit'} </Button> <Button variant="danger" onClick={handleDelete}> Delete </Button> </div> </div> ); }

Step 7: Routing Configuration

// src/App.jsx import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; import { TaskProvider } from './context/TaskContext'; import ProtectedRoute from './routes/ProtectedRoute'; import Layout from './components/layout/Layout'; import Login from './features/auth/Login'; import Register from './features/auth/Register'; import Dashboard from './pages/Dashboard'; export default function App() { return ( <BrowserRouter> <AuthProvider> <TaskProvider> <Routes> <Route path="/login" element={<Login />} /> <Route path="/register" element={<Register />} /> <Route path="/dashboard" element={ <ProtectedRoute> <Layout> <Dashboard /> </Layout> </ProtectedRoute> } /> <Route path="/" element={<Navigate to="/dashboard" replace />} /> </Routes> </TaskProvider> </AuthProvider> </BrowserRouter> ); }
Architecture Benefits:
  • Separation of Concerns: Context for state, services for API, components for UI
  • Reusability: Common components used throughout
  • Scalability: Easy to add new features
  • Maintainability: Clear structure, easy to find code
  • Testability: Each layer can be tested independently
Exercise 1: Extend the task manager with additional features:
  • Add due dates to tasks
  • Implement task priorities (low, medium, high)
  • Add task categories/tags
  • Implement sorting (by date, priority, alphabetical)
  • Add pagination for large task lists
Exercise 2: Improve user experience:
  • Add optimistic updates (update UI before API response)
  • Implement undo functionality for deletions
  • Add toast notifications for success/error messages
  • Implement drag-and-drop for task reordering
  • Add keyboard shortcuts (e.g., Ctrl+N for new task)
Exercise 3: Add testing and deployment:
  • Write unit tests for context providers
  • Write integration tests for task CRUD operations
  • Set up error boundary for graceful error handling
  • Configure environment variables for dev/staging/prod
  • Deploy to Vercel or Netlify