β Module Project: Todo Application
You've mastered the fundamentals of state and interactivity in React! You've learned useState, state management patterns, forms, lists, keys, and conditional rendering. Now it's time to combine everything into a real-world application. In this module project, you'll build a full-featured todo application with categories, filtering, local storage persistence, and all the polish of a production app. This isn't just practiceβyou'll create something genuinely useful! π
π― Project Objectives
By completing this project, you will:
- Build a complete task management application with React and TypeScript
- Manage complex state with useState and proper state updates
- Implement CRUD operations (Create, Read, Update, Delete)
- Create controlled forms with validation
- Handle lists with proper keys and array methods
- Apply conditional rendering for different app states
- Implement filtering and category systems
- Add local storage persistence
- Build a polished, user-friendly interface
- Handle edge cases and error states
Estimated Time: 3-4 hours
Difficulty: Intermediate - applies all Module 3 concepts
π‘ What You'll Build
A production-ready todo application with:
- β Add, edit, and delete todos
- π·οΈ Organize todos by category (Work, Personal, Shopping, etc.)
- π Filter todos (All, Active, Completed)
- β Mark todos as priority
- βοΈ Mark todos as complete/incomplete
- πΎ Persist todos in local storage
- π Display statistics (total, completed, remaining)
- π¨ Beautiful, responsive UI
- π§Ή Bulk actions (clear completed, delete all)
- π± Mobile-friendly design
π Project Sections
π Project Requirements
Let's break down exactly what we're building. A todo application might sound simple, but a well-built one requires careful attention to state management, user experience, and edge cases. This project will prepare you for building any kind of CRUD application.
Core Features
Essential Functionality
- Add Todos - Create new tasks with title, category, and priority
- Display Todos - Show all todos in an organized list
- Complete Todos - Toggle completion status
- Edit Todos - Modify existing todo details
- Delete Todos - Remove individual todos
- Filter Todos - View All, Active, or Completed todos
- Categorize - Organize by Work, Personal, Shopping, etc.
- Priority Levels - Mark important tasks
- Statistics - Show counts and completion percentage
- Persistence - Save todos to local storage
Technical Requirements
Implementation Standards
| Requirement | Details |
|---|---|
| State Management | Use useState for todos, filters, and form state |
| TypeScript | All components, props, and state properly typed |
| Form Handling | Controlled inputs with validation |
| List Rendering | Proper keys and efficient array operations |
| Conditional Rendering | Loading, empty, and error states |
| Component Structure | Reusable, composable components |
| Responsiveness | Works on mobile, tablet, and desktop |
| Accessibility | Keyboard navigation, ARIA labels |
User Stories
π Think Like a User
- As a user, I want to quickly add todos so I can capture tasks as they come up
- As a user, I want to organize todos by category so I can focus on specific areas
- As a user, I want to mark todos as complete so I can track my progress
- As a user, I want to filter todos so I can focus on what's active
- As a user, I want to edit todos so I can fix mistakes or update details
- As a user, I want to see statistics so I know how much I've accomplished
- As a user, I want todos to persist so I don't lose them when I close the app
- As a user, I want to mark priority todos so I can identify urgent tasks
Application Flow
graph TD
A[User Opens App] --> B{Todos in Storage?}
B -->|Yes| C[Load Todos]
B -->|No| D[Show Empty State]
C --> E[Display Todo List]
D --> E
E --> F{User Action}
F -->|Add Todo| G[Show Form]
G --> H[Validate Input]
H --> I[Add to State]
I --> J[Save to Storage]
J --> E
F -->|Toggle Complete| K[Update State]
K --> J
F -->|Delete Todo| L[Remove from State]
L --> J
F -->|Filter| M[Update Filter State]
M --> E
F -->|Edit Todo| N[Show Edit Form]
N --> H
style A fill:#667eea,color:#fff
style E fill:#4CAF50,color:#fff
style J fill:#FF9800,color:#fff
What Makes This Challenging?
β οΈ Key Challenges You'll Face
- State Complexity: Managing multiple pieces of related state (todos, filters, edit mode)
- Immutability: Updating nested objects and arrays correctly
- Derived State: Computing filtered lists and statistics efficiently
- Form State: Handling controlled inputs with multiple fields
- IDs Management: Generating unique IDs for todos
- Storage Sync: Keeping local storage in sync with state
- Edge Cases: Empty lists, validation errors, editing conflicts
- Performance: Avoiding unnecessary re-renders
π οΈ Project Setup & Planning
Before we write any code, let's plan our approach. A little planning now will save hours of refactoring later.
File Structure
Recommended Organization
todo-app/
βββ src/
β βββ App.tsx # Main app component
β βββ App.css # App styles
β βββ main.tsx # Entry point
β βββ types/
β β βββ todo.ts # Type definitions
β βββ components/
β β βββ TodoForm.tsx # Add/Edit todo form
β β βββ TodoList.tsx # List container
β β βββ TodoItem.tsx # Individual todo
β β βββ FilterBar.tsx # Filter controls
β β βββ Statistics.tsx # Stats dashboard
β β βββ EmptyState.tsx # Empty list message
β βββ utils/
β β βββ storage.ts # Local storage helpers
β βββ hooks/
β βββ useTodos.ts # (Optional) Custom hook
βββ index.html
βββ package.json
βββ tsconfig.json
Initial Setup Steps
1. Create the Project
# Create new Vite + React + TypeScript project
npm create vite@latest todo-app -- --template react-ts
# Navigate into project
cd todo-app
# Install dependencies
npm install
# Start development server
npm run dev
Planning Your Components
Component Hierarchy
graph TD
A[App] --> B[TodoForm]
A --> C[FilterBar]
A --> D[Statistics]
A --> E[TodoList]
E --> F1[TodoItem]
E --> F2[TodoItem]
E --> F3[TodoItem]
E --> G[EmptyState]
style A fill:#667eea,color:#fff
style B fill:#4CAF50,color:#fff
style C fill:#2196F3,color:#fff
style D fill:#FF9800,color:#fff
style E fill:#9C27B0,color:#fff
β Component Responsibilities
| Component | Responsibility | State/Props |
|---|---|---|
| App | Manages all state, coordinates components | todos, filter, editingId |
| TodoForm | Handles adding/editing todos | Props: onSubmit, editTodo |
| FilterBar | Controls which todos are visible | Props: filter, onFilterChange |
| Statistics | Displays counts and metrics | Props: todos (for calculations) |
| TodoList | Renders filtered todo items | Props: todos, onToggle, onDelete, onEdit |
| TodoItem | Displays single todo with actions | Props: todo, onToggle, onDelete, onEdit |
| EmptyState | Shows message when no todos | Props: filter (for custom messages) |
State Management Plan
What State Do We Need?
// In App component
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const [editingId, setEditingId] = useState<string | null>(null);
// Derived state (computed, not stored)
const filteredTodos = getFilteredTodos(todos, filter);
const stats = calculateStatistics(todos);
π‘ State Management Principles
- Single Source of Truth: App component owns all state
- State Lifting: Pass state down, pass updaters up
- Immutability: Always create new arrays/objects
- Derived State: Calculate filtered listsβdon't store them
- Minimal State: Only store what can't be computed
π Data Modeling & Types
Let's define our data structures. Good TypeScript types will catch bugs before they happen and make our code self-documenting.
Todo Type Definition
Create src/types/todo.ts
// Core Todo interface
export interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
category: Category;
priority: Priority;
createdAt: Date;
updatedAt: Date;
}
// Category options
export type Category =
| 'work'
| 'personal'
| 'shopping'
| 'health'
| 'other';
// Priority levels
export type Priority = 'low' | 'medium' | 'high';
// Filter options
export type FilterType = 'all' | 'active' | 'completed';
// Category display info
export interface CategoryInfo {
value: Category;
label: string;
icon: string;
color: string;
}
// Priority display info
export interface PriorityInfo {
value: Priority;
label: string;
icon: string;
color: string;
}
// Form data (before creating todo)
export interface TodoFormData {
title: string;
description: string;
category: Category;
priority: Priority;
}
// Statistics
export interface TodoStats {
total: number;
completed: number;
active: number;
completionRate: number;
byCategory: Record<Category, number>;
byPriority: Record<Priority, number>;
}
Constants & Configuration
Category Configuration
export const CATEGORIES: CategoryInfo[] = [
{
value: 'work',
label: 'Work',
icon: 'πΌ',
color: '#3B82F6'
},
{
value: 'personal',
label: 'Personal',
icon: 'π€',
color: '#8B5CF6'
},
{
value: 'shopping',
label: 'Shopping',
icon: 'π',
color: '#10B981'
},
{
value: 'health',
label: 'Health',
icon: 'πͺ',
color: '#EF4444'
},
{
value: 'other',
label: 'Other',
icon: 'π',
color: '#6B7280'
}
];
export const PRIORITIES: PriorityInfo[] = [
{
value: 'low',
label: 'Low',
icon: 'π’',
color: '#10B981'
},
{
value: 'medium',
label: 'Medium',
icon: 'π‘',
color: '#F59E0B'
},
{
value: 'high',
label: 'High',
icon: 'π΄',
color: '#EF4444'
}
];
Utility Functions
Helper Functions for Todos
// Generate unique ID
export const generateId = (): string => {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
// Create new todo
export const createTodo = (formData: TodoFormData): Todo => {
return {
id: generateId(),
title: formData.title.trim(),
description: formData.description?.trim(),
completed: false,
category: formData.category,
priority: formData.priority,
createdAt: new Date(),
updatedAt: new Date()
};
};
// Get category info
export const getCategoryInfo = (category: Category): CategoryInfo => {
return CATEGORIES.find(c => c.value === category) || CATEGORIES[4];
};
// Get priority info
export const getPriorityInfo = (priority: Priority): PriorityInfo => {
return PRIORITIES.find(p => p.value === priority) || PRIORITIES[0];
};
Why These Types Matter
β Benefits of Strong Typing
- Autocomplete: Your editor knows what properties exist
- Error Prevention: Catch typos and mistakes at compile time
- Refactoring Safety: Change types and see where updates are needed
- Documentation: Types explain what data looks like
- Confidence: TypeScript ensures data structure consistency
Example: Type Safety in Action
// β Without types (JavaScript)
const handleToggle = (id) => {
// What's the type of id? String? Number?
setTodos(todos.map(todo => {
if (todo.id === id) {
return { ...todo, complete: !todo.complete }; // Typo! Should be "completed"
}
return todo;
}));
};
// β
With types (TypeScript)
const handleToggle = (id: string) => {
setTodos(todos.map(todo => {
if (todo.id === id) {
return { ...todo, complete: !todo.complete };
// TypeScript error: Property 'complete' does not exist on type 'Todo'
}
return todo;
}));
};
ποΈ App Architecture
Now let's build the main App component that will orchestrate everything. This is where all our state lives and where we coordinate between components.
App Component Structure
High-Level Overview
import { useState, useEffect } from 'react';
import { Todo, FilterType, TodoFormData } from './types/todo';
import { createTodo } from './types/todo';
import TodoForm from './components/TodoForm';
import FilterBar from './components/FilterBar';
import Statistics from './components/Statistics';
import TodoList from './components/TodoList';
import './App.css';
const App: React.FC = () => {
// State
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const [editingId, setEditingId] = useState<string | null>(null);
// Load from localStorage on mount
useEffect(() => {
// Will implement in Section 11
}, []);
// Save to localStorage whenever todos change
useEffect(() => {
// Will implement in Section 11
}, [todos]);
// Event handlers (CRUD operations)
const handleAddTodo = (formData: TodoFormData) => {
// Will implement in Section 5
};
const handleUpdateTodo = (id: string, formData: TodoFormData) => {
// Will implement in Section 5
};
const handleToggleTodo = (id: string) => {
// Will implement in Section 5
};
const handleDeleteTodo = (id: string) => {
// Will implement in Section 5
};
const handleClearCompleted = () => {
// Will implement in Section 5
};
// Derived state (computed)
const filteredTodos = getFilteredTodos(todos, filter);
const editingTodo = editingId ? todos.find(t => t.id === editingId) : null;
return (
<div className="app">
<header className="app-header">
<h1>β
Todo App</h1>
<p>Stay organized and productive</p>
</header>
<main className="app-main">
<Statistics todos={todos} />
<TodoForm
onSubmit={editingId ? handleUpdateTodo : handleAddTodo}
editTodo={editingTodo}
onCancelEdit={() => setEditingId(null)}
/>
<FilterBar
filter={filter}
onFilterChange={setFilter}
onClearCompleted={handleClearCompleted}
hasCompleted={todos.some(t => t.completed)}
/>
<TodoList
todos={filteredTodos}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
onEdit={(id) => setEditingId(id)}
filter={filter}
/>
</main>
</div>
);
};
export default App;
Helper Function: Filter Todos
Computing Filtered Lists
const getFilteredTodos = (todos: Todo[], filter: FilterType): Todo[] => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
case 'all':
default:
return todos;
}
};
π‘ Architecture Decisions
- State Location: All state in App component (centralized)
- Data Flow: Unidirectionalβstate down, events up
- Derived State: Filtered todos computed on each render (fast enough for this app)
- Edit Mode: Store editing ID, not editing state in each item
- Responsibilities: App coordinates, components render
Data Flow Diagram
graph TB
A[App State: todos] --> B[Statistics]
A --> C[TodoForm]
A --> D[FilterBar]
A --> E[Filtered Todos]
E --> F[TodoList]
F --> G[TodoItem 1]
F --> H[TodoItem 2]
G -->|onToggle| A
G -->|onDelete| A
G -->|onEdit| A
C -->|onSubmit| A
D -->|onFilterChange| A
style A fill:#667eea,color:#fff
style E fill:#4CAF50,color:#fff
π Building State Management
Now let's implement all the CRUD operations. These are the heart of our todo appβcreating, reading, updating, and deleting todos.
Add Todo Handler
Creating New Todos
const handleAddTodo = (formData: TodoFormData) => {
// Create new todo with generated ID and timestamps
const newTodo = createTodo(formData);
// Add to beginning of array (newest first)
setTodos(prevTodos => [newTodo, ...prevTodos]);
// Note: We return new array to maintain immutability
};
Update Todo Handler
Editing Existing Todos
const handleUpdateTodo = (id: string, formData: TodoFormData) => {
setTodos(prevTodos =>
prevTodos.map(todo => {
if (todo.id === id) {
// Create updated todo preserving some original data
return {
...todo,
title: formData.title.trim(),
description: formData.description?.trim(),
category: formData.category,
priority: formData.priority,
updatedAt: new Date()
};
}
return todo;
})
);
// Exit edit mode
setEditingId(null);
};
Toggle Complete Handler
Marking Todos as Complete/Incomplete
const handleToggleTodo = (id: string) => {
setTodos(prevTodos =>
prevTodos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed,
updatedAt: new Date()
};
}
return todo;
})
);
};
Delete Todo Handler
Removing Todos
const handleDeleteTodo = (id: string) => {
// Filter out the todo with matching id
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
// If we were editing this todo, exit edit mode
if (editingId === id) {
setEditingId(null);
}
};
Bulk Actions
Clear All Completed Todos
const handleClearCompleted = () => {
// Keep only non-completed todos
setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
};
β State Update Best Practices
- Use Functional Updates:
setTodos(prev => ...)ensures you have latest state - Maintain Immutability: Always return new arrays/objects, never mutate
- Use Array Methods:
map,filterfor clean, declarative updates - Update Timestamps: Track when todos are modified
- Clean Up Side Effects: Exit edit mode when deleting
Common Mistakes to Avoid
// β DON'T: Mutate state directly
const handleToggle = (id: string) => {
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed; // MUTATION!
setTodos(todos); // React won't detect change
};
// β
DO: Create new array with updated item
const handleToggle = (id: string) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
// β DON'T: Forget to use previous state
const handleAddTodo = (formData: TodoFormData) => {
const newTodo = createTodo(formData);
setTodos([newTodo, ...todos]); // Race condition possible!
};
// β
DO: Use functional update
const handleAddTodo = (formData: TodoFormData) => {
const newTodo = createTodo(formData);
setTodos(prevTodos => [newTodo, ...prevTodos]);
};
π Todo Input Form
The form is where users add and edit todos. It needs validation, controlled inputs, and support for both create and update modes.
TodoForm Component
Create src/components/TodoForm.tsx
import { useState, useEffect, FormEvent } from 'react';
import { Todo, TodoFormData, Category, Priority, CATEGORIES, PRIORITIES } from '../types/todo';
interface TodoFormProps {
onSubmit: (formData: TodoFormData) => void;
editTodo?: Todo | null;
onCancelEdit?: () => void;
}
const TodoForm: React.FC<TodoFormProps> = ({ onSubmit, editTodo, onCancelEdit }) => {
// Form state
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState<Category>('personal');
const [priority, setPriority] = useState<Priority>('medium');
const [errors, setErrors] = useState<Record<string, string>>({});
// Populate form when editing
useEffect(() => {
if (editTodo) {
setTitle(editTodo.title);
setDescription(editTodo.description || '');
setCategory(editTodo.category);
setPriority(editTodo.priority);
}
}, [editTodo]);
// Validation
const validate = (): boolean => {
const newErrors: Record<string, string> = {};
if (title.trim().length === 0) {
newErrors.title = 'Title is required';
} else if (title.trim().length < 3) {
newErrors.title = 'Title must be at least 3 characters';
} else if (title.trim().length > 100) {
newErrors.title = 'Title must be less than 100 characters';
}
if (description.trim().length > 500) {
newErrors.description = 'Description must be less than 500 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Handle submit
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validate()) {
return;
}
const formData: TodoFormData = {
title,
description,
category,
priority
};
onSubmit(formData);
// Reset form if not editing
if (!editTodo) {
setTitle('');
setDescription('');
setCategory('personal');
setPriority('medium');
}
setErrors({});
};
// Handle cancel
const handleCancel = () => {
setTitle('');
setDescription('');
setCategory('personal');
setPriority('medium');
setErrors({});
onCancelEdit?.();
};
return (
<form className="todo-form" onSubmit={handleSubmit}>
<div className="form-header">
<h2>{editTodo ? 'βοΈ Edit Todo' : 'β Add New Todo'}</h2>
</div>
{/* Title Input */}
<div className="form-group">
<label htmlFor="title">
Title <span className="required">*</span>
</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What needs to be done?"
className={errors.title ? 'error' : ''}
aria-invalid={!!errors.title}
aria-describedby={errors.title ? 'title-error' : undefined}
/>
{errors.title && (
<span className="error-message" id="title-error">
{errors.title}
</span>
)}
</div>
{/* Description Input */}
<div className="form-group">
<label htmlFor="description">Description (Optional)</label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add more details..."
rows={3}
className={errors.description ? 'error' : ''}
aria-invalid={!!errors.description}
/>
{errors.description && (
<span className="error-message">{errors.description}</span>
)}
</div>
{/* Category Select */}
<div className="form-group">
<label htmlFor="category">Category</label>
<select
id="category"
value={category}
onChange={(e) => setCategory(e.target.value as Category)}
>
{CATEGORIES.map(cat => (
<option key={cat.value} value={cat.value}>
{cat.icon} {cat.label}
</option>
))}
</select>
</div>
{/* Priority Select */}
<div className="form-group">
<label htmlFor="priority">Priority</label>
<select
id="priority"
value={priority}
onChange={(e) => setPriority(e.target.value as Priority)}
>
{PRIORITIES.map(pri => (
<option key={pri.value} value={pri.value}>
{pri.icon} {pri.label}
</option>
))}
</select>
</div>
{/* Action Buttons */}
<div className="form-actions">
<button type="submit" className="btn btn-primary">
{editTodo ? 'πΎ Update' : 'β Add Todo'}
</button>
{editTodo && (
<button
type="button"
onClick={handleCancel}
className="btn btn-secondary"
>
β Cancel
</button>
)}
</div>
</form>
);
};
export default TodoForm;
Form Features Explained
π‘ Key Form Concepts
- Controlled Inputs: Value and onChange for each input
- Dual Mode: Same form for add and edit operations
- Validation: Client-side validation with error messages
- useEffect: Populates form when editing
- Type Safety: Properly typed form data and handlers
- Accessibility: Labels, ARIA attributes, error associations
- Reset Logic: Clear form after adding, not after editing
Validation Logic
// Validation rules
const rules = {
title: {
required: true,
minLength: 3,
maxLength: 100
},
description: {
maxLength: 500
}
};
// Why validate?
// - Prevent empty todos
// - Ensure data quality
// - Provide user feedback
// - Avoid backend errors
π Todo List Component
The TodoList component renders all visible todos. It handles the empty state and passes actions to individual items.
TodoList Component
Create src/components/TodoList.tsx
import { Todo, FilterType } from '../types/todo';
import TodoItem from './TodoItem';
import EmptyState from './EmptyState';
interface TodoListProps {
todos: Todo[];
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string) => void;
filter: FilterType;
}
const TodoList: React.FC<TodoListProps> = ({
todos,
onToggle,
onDelete,
onEdit,
filter
}) => {
// Show empty state if no todos
if (todos.length === 0) {
return <EmptyState filter={filter} />;
}
return (
<div className="todo-list">
<div className="todo-list-header">
<h3>
{filter === 'all' && `All Todos (${todos.length})`}
{filter === 'active' && `Active Todos (${todos.length})`}
{filter === 'completed' && `Completed Todos (${todos.length})`}
</h3>
</div>
<ul className="todo-items">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
onEdit={onEdit}
/>
))}
</ul>
</div>
);
};
export default TodoList;
EmptyState Component
Create src/components/EmptyState.tsx
import { FilterType } from '../types/todo';
interface EmptyStateProps {
filter: FilterType;
}
const EmptyState: React.FC<EmptyStateProps> = ({ filter }) => {
const getMessage = () => {
switch (filter) {
case 'active':
return {
icon: 'π',
title: 'All Done!',
message: 'You have no active todos. Great job!'
};
case 'completed':
return {
icon: 'π',
title: 'No Completed Todos',
message: 'Complete some todos to see them here.'
};
case 'all':
default:
return {
icon: 'π',
title: 'No Todos Yet',
message: 'Add your first todo to get started!'
};
}
};
const { icon, title, message } = getMessage();
return (
<div className="empty-state">
<div className="empty-state-icon">{icon}</div>
<h3>{title}</h3>
<p>{message}</p>
</div>
);
};
export default EmptyState;
β List Rendering Best Practices
- Always Use Keys: Unique, stable
keyprop for each item - Empty State: Show helpful message when list is empty
- Map Method: Transform array of data to array of JSX
- Prop Drilling: Pass callbacks to children
- Conditional Headers: Show appropriate count and label
Why Keys Matter
// β DON'T: Use array index as key
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
// Problem: Keys change when items reorder!
))}
// β
DO: Use stable unique ID
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
// Stable key = React can efficiently update
))}
π Todo Item Component
Each todo item displays the task details and provides actions to toggle, edit, or delete it.
TodoItem Component
Create src/components/TodoItem.tsx
import { Todo, getCategoryInfo, getPriorityInfo } from '../types/todo';
interface TodoItemProps {
todo: Todo;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
onEdit: (id: string) => void;
}
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete, onEdit }) => {
const categoryInfo = getCategoryInfo(todo.category);
const priorityInfo = getPriorityInfo(todo.priority);
const handleToggle = () => {
onToggle(todo.id);
};
const handleEdit = () => {
onEdit(todo.id);
};
const handleDelete = () => {
if (window.confirm('Are you sure you want to delete this todo?')) {
onDelete(todo.id);
}
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
{/* Checkbox */}
<div className="todo-checkbox">
<input
type="checkbox"
checked={todo.completed}
onChange={handleToggle}
id={`todo-${todo.id}`}
aria-label={`Mark "${todo.title}" as ${todo.completed ? 'incomplete' : 'complete'}`}
/>
<label htmlFor={`todo-${todo.id}`}></label>
</div>
{/* Todo Content */}
<div className="todo-content">
<div className="todo-header">
<h4 className={todo.completed ? 'strikethrough' : ''}>
{todo.title}
</h4>
<div className="todo-badges">
<span
className="badge badge-category"
style={{ backgroundColor: categoryInfo.color }}
title={categoryInfo.label}
>
{categoryInfo.icon} {categoryInfo.label}
</span>
<span
className="badge badge-priority"
style={{ backgroundColor: priorityInfo.color }}
title={`${priorityInfo.label} priority`}
>
{priorityInfo.icon}
</span>
</div>
</div>
{todo.description && (
<p className="todo-description">{todo.description}</p>
)}
<div className="todo-meta">
<span className="todo-date">
Created: {new Date(todo.createdAt).toLocaleDateString()}
</span>
{todo.updatedAt !== todo.createdAt && (
<span className="todo-date">
Updated: {new Date(todo.updatedAt).toLocaleDateString()}
</span>
)}
</div>
</div>
{/* Actions */}
<div className="todo-actions">
<button
onClick={handleEdit}
className="btn-icon"
title="Edit todo"
aria-label={`Edit "${todo.title}"`}
>
βοΈ
</button>
<button
onClick={handleDelete}
className="btn-icon btn-delete"
title="Delete todo"
aria-label={`Delete "${todo.title}"`}
>
ποΈ
</button>
</div>
</li>
);
};
export default TodoItem;
Item Features
π‘ Component Highlights
- Visual Feedback: Different styles for completed todos
- Badges: Category and priority indicators with colors
- Metadata: Show creation and update timestamps
- Confirmation: Ask before deleting
- Accessibility: Proper labels and ARIA attributes
- Strikethrough: Completed todos get strikethrough text
Event Handler Pattern
// Pattern: Create wrapper functions
const handleToggle = () => {
onToggle(todo.id);
};
// Why?
// 1. Passes the correct ID to parent
// 2. Keeps component flexible
// 3. Can add logic before calling parent
// 4. Cleaner JSX
// Alternative (inline):
onClick={() => onToggle(todo.id)}
// Both work, wrapper is cleaner for complex logic
π Filter Controls
The filter bar lets users switch between viewing all todos, only active ones, or only completed ones. It also provides bulk actions.
FilterBar Component
Create src/components/FilterBar.tsx
import { FilterType } from '../types/todo';
interface FilterBarProps {
filter: FilterType;
onFilterChange: (filter: FilterType) => void;
onClearCompleted: () => void;
hasCompleted: boolean;
}
const FilterBar: React.FC<FilterBarProps> = ({
filter,
onFilterChange,
onClearCompleted,
hasCompleted
}) => {
const filters: { value: FilterType; label: string; icon: string }[] = [
{ value: 'all', label: 'All', icon: 'π' },
{ value: 'active', label: 'Active', icon: 'β³' },
{ value: 'completed', label: 'Completed', icon: 'β
' }
];
return (
<div className="filter-bar">
<div className="filter-buttons">
{filters.map(f => (
<button
key={f.value}
onClick={() => onFilterChange(f.value)}
className={`filter-btn ${filter === f.value ? 'active' : ''}`}
aria-pressed={filter === f.value}
>
{f.icon} {f.label}
</button>
))}
</div>
{hasCompleted && (
<button
onClick={onClearCompleted}
className="btn-clear-completed"
title="Remove all completed todos"
>
π§Ή Clear Completed
</button>
)}
</div>
);
};
export default FilterBar;
Filter Logic
How Filtering Works
// In App.tsx
const getFilteredTodos = (todos: Todo[], filter: FilterType): Todo[] => {
switch (filter) {
case 'active':
// Show only incomplete todos
return todos.filter(todo => !todo.completed);
case 'completed':
// Show only completed todos
return todos.filter(todo => todo.completed);
case 'all':
default:
// Show all todos
return todos;
}
};
// Usage
const filteredTodos = getFilteredTodos(todos, filter);
π‘ Filter Design Patterns
- Active State: Highlight currently selected filter
- Conditional Rendering: Only show "Clear Completed" if there are completed todos
- Accessibility: Use
aria-pressedfor toggle buttons - Icon + Text: Makes buttons more visual and understandable
- Derived Data: hasCompleted prop computed by parent
π Statistics Dashboard
Show users their progress with statistics and metrics. This makes the app feel more complete and motivating!
Statistics Component
Create src/components/Statistics.tsx
import { Todo, TodoStats } from '../types/todo';
interface StatisticsProps {
todos: Todo[];
}
const Statistics: React.FC<StatisticsProps> = ({ todos }) => {
// Calculate statistics
const stats: TodoStats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
completionRate: todos.length > 0
? Math.round((todos.filter(t => t.completed).length / todos.length) * 100)
: 0,
byCategory: {
work: todos.filter(t => t.category === 'work').length,
personal: todos.filter(t => t.category === 'personal').length,
shopping: todos.filter(t => t.category === 'shopping').length,
health: todos.filter(t => t.category === 'health').length,
other: todos.filter(t => t.category === 'other').length
},
byPriority: {
low: todos.filter(t => t.priority === 'low').length,
medium: todos.filter(t => t.priority === 'medium').length,
high: todos.filter(t => t.priority === 'high').length
}
};
return (
<div className="statistics">
<h2>π Your Progress</h2>
{/* Main Stats */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon">π</div>
<div className="stat-value">{stats.total}</div>
<div className="stat-label">Total Todos</div>
</div>
<div className="stat-card stat-active">
<div className="stat-icon">β³</div>
<div className="stat-value">{stats.active}</div>
<div className="stat-label">Active</div>
</div>
<div className="stat-card stat-completed">
<div className="stat-icon">β
</div>
<div className="stat-value">{stats.completed}</div>
<div className="stat-label">Completed</div>
</div>
<div className="stat-card stat-completion">
<div className="stat-icon">π</div>
<div className="stat-value">{stats.completionRate}%</div>
<div className="stat-label">Completion Rate</div>
</div>
</div>
{/* Progress Bar */}
{stats.total > 0 && (
<div className="progress-section">
<div className="progress-bar-container">
<div
className="progress-bar-fill"
style={{ width: `${stats.completionRate}%` }}
role="progressbar"
aria-valuenow={stats.completionRate}
aria-valuemin={0}
aria-valuemax={100}
></div>
</div>
<p className="progress-text">
{stats.completed} of {stats.total} todos completed
</p>
</div>
)}
{/* Category Breakdown */}
{stats.total > 0 && (
<details className="stats-details">
<summary>π View Detailed Breakdown</summary>
<div className="breakdown">
<div className="breakdown-section">
<h4>By Category</h4>
<ul>
{stats.byCategory.work > 0 && (
<li>πΌ Work: {stats.byCategory.work}</li>
)}
{stats.byCategory.personal > 0 && (
<li>π€ Personal: {stats.byCategory.personal}</li>
)}
{stats.byCategory.shopping > 0 && (
<li>π Shopping: {stats.byCategory.shopping}</li>
)}
{stats.byCategory.health > 0 && (
<li>πͺ Health: {stats.byCategory.health}</li>
)}
{stats.byCategory.other > 0 && (
<li>π Other: {stats.byCategory.other}</li>
)}
</ul>
</div>
<div className="breakdown-section">
<h4>By Priority</h4>
<ul>
{stats.byPriority.high > 0 && (
<li>π΄ High: {stats.byPriority.high}</li>
)}
{stats.byPriority.medium > 0 && (
<li>π‘ Medium: {stats.byPriority.medium}</li>
)}
{stats.byPriority.low > 0 && (
<li>π’ Low: {stats.byPriority.low}</li>
)}
</ul>
</div>
</div>
</details>
)}
</div>
);
};
export default Statistics;
Computed Statistics
β Statistics Best Practices
- Compute on Render: Stats are derived from todos array
- Don't Store Stats: They change whenever todos change
- Visual Progress: Progress bar shows completion visually
- Conditional Display: Hide breakdowns when there are no todos
- Collapsible Details: Use
<details>for advanced stats - Accessibility: Progress bar has proper ARIA attributes
Performance Note
// These calculations happen on every render
// For 100s of todos, this is fine
// For 1000s+ todos, consider useMemo:
const stats = useMemo(() => {
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
// ... rest of calculations
};
}, [todos]);
// But for this app, simple calculation is sufficient!
πΎ Local Storage Persistence
Make todos persist across browser sessions by saving to local storage. Users hate losing their data!
Storage Utility Functions
Create src/utils/storage.ts
import { Todo } from '../types/todo';
const STORAGE_KEY = 'todos_app_data';
// Save todos to localStorage
export const saveTodos = (todos: Todo[]): void => {
try {
const serialized = JSON.stringify(todos);
localStorage.setItem(STORAGE_KEY, serialized);
} catch (error) {
console.error('Error saving todos to localStorage:', error);
}
};
// Load todos from localStorage
export const loadTodos = (): Todo[] => {
try {
const serialized = localStorage.getItem(STORAGE_KEY);
if (serialized === null) {
return [];
}
const parsed = JSON.parse(serialized);
// Convert date strings back to Date objects
return parsed.map((todo: any) => ({
...todo,
createdAt: new Date(todo.createdAt),
updatedAt: new Date(todo.updatedAt)
}));
} catch (error) {
console.error('Error loading todos from localStorage:', error);
return [];
}
};
// Clear all todos from storage
export const clearStorage = (): void => {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('Error clearing localStorage:', error);
}
};
Integrating Storage in App
Add useEffect Hooks to App.tsx
import { useState, useEffect } from 'react';
import { loadTodos, saveTodos } from './utils/storage';
const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
// Load todos from localStorage on mount
useEffect(() => {
const savedTodos = loadTodos();
if (savedTodos.length > 0) {
setTodos(savedTodos);
}
}, []); // Empty dependency array = run once on mount
// Save todos to localStorage whenever they change
useEffect(() => {
if (todos.length > 0) {
saveTodos(todos);
}
}, [todos]); // Re-run whenever todos change
// ... rest of component
};
How Storage Works
π‘ LocalStorage Explained
graph LR
A[User Adds Todo] --> B[State Updates]
B --> C[useEffect Triggers]
C --> D[Save to LocalStorage]
E[Page Reloads] --> F[useEffect on Mount]
F --> G[Load from LocalStorage]
G --> H[Set State]
H --> I[Render Todos]
style A fill:#667eea,color:#fff
style D fill:#4CAF50,color:#fff
style G fill:#FF9800,color:#fff
Storage Considerations
β οΈ Important Notes
- JSON Only: LocalStorage stores stringsβwe serialize/deserialize with JSON
- Date Handling: Dates become strings in JSONβconvert back to Date objects
- Error Handling: Try/catch in case localStorage is disabled
- Size Limits: ~5-10MB limit per domain (plenty for todos)
- Sync vs Async: LocalStorage is synchronous (blocking)
- Privacy: Data visible to anyone with browser access
- No Expiration: Data persists until explicitly cleared
Testing Storage
// Check what's in storage
console.log(localStorage.getItem('todos_app_data'));
// Clear storage (for testing)
localStorage.removeItem('todos_app_data');
// Manually set data
const testTodos = [/* ... */];
localStorage.setItem('todos_app_data', JSON.stringify(testTodos));
π¨ Styling the Application
Let's make our todo app look professional! Here's a complete CSS stylesheet that covers all components.
Main Stylesheet
Update src/App.css
/* ===== CSS Variables ===== */
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--success-color: #4CAF50;
--warning-color: #FF9800;
--danger-color: #f44336;
--text-color: #333;
--text-light: #666;
--bg-color: #f5f7fa;
--card-bg: white;
--border-color: #e0e0e0;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* ===== Base Styles ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* ===== App Header ===== */
.app-header {
text-align: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.app-header p {
color: var(--text-light);
font-size: 1.1rem;
}
/* ===== Statistics ===== */
.statistics {
background: var(--card-bg);
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.statistics h2 {
margin-bottom: 1rem;
color: var(--text-color);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
}
.stat-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.progress-bar-container {
width: 100%;
height: 20px;
background: var(--border-color);
border-radius: 10px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--success-color), #66bb6a);
transition: width 0.3s ease;
}
/* ===== Todo Form ===== */
.todo-form {
background: var(--card-bg);
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.required {
color: var(--danger-color);
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
}
.form-group input.error,
.form-group textarea.error {
border-color: var(--danger-color);
}
.error-message {
color: var(--danger-color);
font-size: 0.875rem;
margin-top: 0.25rem;
display: block;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
}
/* ===== Buttons ===== */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.btn-secondary {
background: var(--border-color);
color: var(--text-color);
}
.btn-icon {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
transition: transform 0.2s;
}
.btn-icon:hover {
transform: scale(1.2);
}
/* ===== Filter Bar ===== */
.filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--card-bg);
padding: 1rem;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.filter-buttons {
display: flex;
gap: 0.5rem;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 2px solid var(--border-color);
background: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
/* ===== Todo List ===== */
.todo-list {
background: var(--card-bg);
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--shadow);
}
.todo-items {
list-style: none;
}
.todo-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
transition: background 0.2s;
}
.todo-item:hover {
background: var(--bg-color);
}
.todo-item.completed {
opacity: 0.6;
}
.todo-checkbox input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-content {
flex: 1;
}
.todo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.strikethrough {
text-decoration: line-through;
}
.todo-badges {
display: flex;
gap: 0.5rem;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
color: white;
}
/* ===== Empty State ===== */
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-light);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.app {
padding: 1rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.filter-bar {
flex-direction: column;
gap: 1rem;
}
}
β CSS Features
- CSS Variables: Easy theme customization
- Gradients: Modern, attractive backgrounds
- Shadows: Depth and elevation
- Transitions: Smooth animations
- Flexbox/Grid: Flexible layouts
- Responsive: Mobile-friendly breakpoints
β Complete Solution
Here's the complete App.tsx with everything integrated!
Complete App.tsx
import { useState, useEffect } from 'react';
import { Todo, FilterType, TodoFormData, createTodo } from './types/todo';
import { loadTodos, saveTodos } from './utils/storage';
import TodoForm from './components/TodoForm';
import FilterBar from './components/FilterBar';
import Statistics from './components/Statistics';
import TodoList from './components/TodoList';
import './App.css';
const App: React.FC = () => {
// State
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const [editingId, setEditingId] = useState<string | null>(null);
// Load from localStorage on mount
useEffect(() => {
const savedTodos = loadTodos();
if (savedTodos.length > 0) {
setTodos(savedTodos);
}
}, []);
// Save to localStorage whenever todos change
useEffect(() => {
if (todos.length >= 0) {
saveTodos(todos);
}
}, [todos]);
// Add new todo
const handleAddTodo = (formData: TodoFormData) => {
const newTodo = createTodo(formData);
setTodos(prevTodos => [newTodo, ...prevTodos]);
};
// Update existing todo
const handleUpdateTodo = (id: string, formData: TodoFormData) => {
setTodos(prevTodos =>
prevTodos.map(todo => {
if (todo.id === id) {
return {
...todo,
title: formData.title.trim(),
description: formData.description?.trim(),
category: formData.category,
priority: formData.priority,
updatedAt: new Date()
};
}
return todo;
})
);
setEditingId(null);
};
// Toggle completion status
const handleToggleTodo = (id: string) => {
setTodos(prevTodos =>
prevTodos.map(todo => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed,
updatedAt: new Date()
};
}
return todo;
})
);
};
// Delete todo
const handleDeleteTodo = (id: string) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
if (editingId === id) {
setEditingId(null);
}
};
// Clear all completed todos
const handleClearCompleted = () => {
setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
};
// Get filtered todos
const getFilteredTodos = (): Todo[] => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
case 'all':
default:
return todos;
}
};
const filteredTodos = getFilteredTodos();
const editingTodo = editingId ? todos.find(t => t.id === editingId) : null;
return (
<div className="app">
<header className="app-header">
<h1>β
Todo App</h1>
<p>Stay organized and productive</p>
</header>
<main className="app-main">
<Statistics todos={todos} />
<TodoForm
onSubmit={editingId
? (formData) => handleUpdateTodo(editingId, formData)
: handleAddTodo
}
editTodo={editingTodo}
onCancelEdit={() => setEditingId(null)}
/>
<FilterBar
filter={filter}
onFilterChange={setFilter}
onClearCompleted={handleClearCompleted}
hasCompleted={todos.some(t => t.completed)}
/>
<TodoList
todos={filteredTodos}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
onEdit={(id) => setEditingId(id)}
filter={filter}
/>
</main>
</div>
);
};
export default App;
Testing Your App
π‘ How to Test
- Add Todos: Create several todos with different categories and priorities
- Complete Todos: Toggle checkboxes and watch statistics update
- Filter Todos: Switch between All, Active, and Completed filters
- Edit Todos: Click edit button, modify, and save
- Delete Todos: Remove individual todos
- Clear Completed: Use bulk action to remove all completed
- Refresh Page: Verify todos persist in localStorage
- Test Mobile: Resize browser to test responsive design
Common Issues & Solutions
Troubleshooting
| Issue | Solution |
|---|---|
| Todos not persisting | Check browser console for errors, verify localStorage is enabled |
| TypeScript errors | Ensure all types are imported correctly from types/todo.ts |
| Styling not working | Verify App.css is imported in App.tsx |
| Form not clearing | Check that form reset only happens after add, not edit |
| Statistics wrong | Verify filter logic is correct in calculations |
π Enhancements & Extensions
Congratulations on building a complete todo app! Here are ideas to take it further:
Beginner Enhancements
β Easy Additions
- Due Dates: Add date picker for todo deadlines
- Sort Options: Sort by date, priority, or category
- Search: Filter todos by search term
- Dark Mode: Add theme toggle
- Animations: Add enter/exit animations for todos
- Emoji Picker: Let users add emojis to titles
- Export/Import: Download todos as JSON
- Keyboard Shortcuts: Add shortcuts for common actions
Intermediate Enhancements
π‘ More Complex Features
- Subtasks: Add nested todo items
- Tags: Multiple tags per todo instead of single category
- Recurring Todos: Daily, weekly, monthly tasks
- Drag & Drop: Reorder todos by dragging
- Undo/Redo: Implement action history
- Multiple Lists: Different todo lists (Projects)
- Collaboration: Share lists with others
- Notifications: Browser notifications for due dates
Advanced Enhancements
β‘ Advanced Features
- Backend Integration: Save to a real database
- User Authentication: Login and user accounts
- Sync Across Devices: Cloud sync
- Real-time Collaboration: Multiple users editing
- AI Suggestions: Smart task recommendations
- Analytics Dashboard: Productivity insights
- API Integrations: Calendar, email, Slack
- Progressive Web App: Install as mobile app
Example: Adding Search
Implementing Search Feature
// Add search state
const [searchTerm, setSearchTerm] = useState('');
// Update filter function
const getFilteredTodos = (): Todo[] => {
let filtered = todos;
// Apply status filter
switch (filter) {
case 'active':
filtered = filtered.filter(todo => !todo.completed);
break;
case 'completed':
filtered = filtered.filter(todo => todo.completed);
break;
}
// Apply search filter
if (searchTerm.trim()) {
filtered = filtered.filter(todo =>
todo.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
todo.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return filtered;
};
// Add search input to FilterBar
<input
type="search"
placeholder="Search todos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
Example: Adding Due Dates
Implementing Due Date Feature
// Update Todo interface
export interface Todo {
// ... existing fields
dueDate?: Date;
}
// Add to TodoForm
<div className="form-group">
<label htmlFor="dueDate">Due Date (Optional)</label>
<input
type="date"
id="dueDate"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
/>
</div>
// Display in TodoItem
{todo.dueDate && (
<span className={`due-date ${isOverdue(todo.dueDate) ? 'overdue' : ''}`}>
π
Due: {formatDate(todo.dueDate)}
</span>
)}
Performance Optimization Ideas
When Your App Grows
// 1. Memoize expensive calculations
const stats = useMemo(() => calculateStatistics(todos), [todos]);
// 2. Memoize child components
const MemoizedTodoItem = memo(TodoItem);
// 3. Debounce search input
const debouncedSearch = useMemo(
() => debounce((term: string) => setSearchTerm(term), 300),
[]
);
// 4. Virtual scrolling for 100s+ items
import { FixedSizeList } from 'react-window';
// 5. Lazy load components
const Statistics = lazy(() => import('./components/Statistics'));
Learning Path
π What's Next?
You've completed Module 3! Here's what you've mastered:
- β useState for complex state management
- β Controlled forms with validation
- β List rendering with proper keys
- β Conditional rendering patterns
- β CRUD operations
- β Local storage integration
- β Component composition
- β TypeScript type safety
Next Module: Module 4 - Side Effects and Data Fetching
You'll learn useEffect, data fetching, custom hooks, and more!
Project Checklist
Before You're Done
- β All features working correctly
- β No TypeScript errors
- β Form validation working
- β Todos persist after refresh
- β All filters working
- β Edit mode functioning properly
- β Statistics calculating correctly
- β Responsive on mobile
- β No console errors
- β Empty states display properly
Share Your Work!
β Deployment Options
Deploy your todo app for free:
- Vercel:
npm install -g vercelβvercel - Netlify: Drag & drop build folder to netlify.com
- GitHub Pages: Push to GitHub and enable Pages
- Render: Connect GitHub repo and deploy
# Build for production
npm run build
# The dist/ folder contains your deployable app
Congratulations! π
π You Did It!
You've successfully built a complete, production-ready todo application using React and TypeScript. This is a significant achievement that demonstrates your understanding of:
- Component-based architecture
- State management principles
- Form handling and validation
- List rendering and keys
- Conditional rendering patterns
- Data persistence
- TypeScript type safety
You're now ready to tackle more complex React applications!