Project Overview
Welcome to your weekend project! Just as a personal planner helps you organize your daily tasks, you'll build a Todo application that allows users to Create, Read, Update, and Delete (CRUD) their tasks. This project will reinforce all the React concepts you've learned this week: components, state management, event handling, list rendering, and conditional rendering.
George Polya's 4-Step Problem Solving Method
graph TD
A[1. Understand the Problem] --> B[2. Devise a Plan]
B --> C[3. Carry Out the Plan]
C --> D[4. Look Back and Reflect]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#9cf,stroke:#333,stroke-width:2px
style C fill:#9f9,stroke:#333,stroke-width:2px
style D fill:#ff9,stroke:#333,stroke-width:2px
Step 1: Understand the Problem
We need to build a Todo application with the following features:
- Create: Add new todo items
- Read: Display all todo items
- Update: Mark todos as complete/incomplete and edit their text
- Delete: Remove todo items
Expected Input:
- Text input for new todos
- Click events for completing/editing/deleting todos
- Updated text for editing existing todos
Expected Output:
- List of todo items displayed on screen
- Visual indication of completed todos
- Ability to add, edit, complete, and delete todos
- Form inputs for creating and editing todos
Step 2: Devise a Plan
Whiteboard Plan:
- Create basic React project structure
- Design data model for todo items
- Build main App component with state
- Create TodoList component to display todos
- Create TodoItem component for individual todos
- Create TodoForm component for adding todos
- Implement CRUD operations:
- Add todo (Create)
- Display todos (Read)
- Toggle completion (Update)
- Edit todo text (Update)
- Delete todo (Delete)
- Add styling and user feedback
graph TD
A[App Component] --> B[TodoForm]
A --> C[TodoList]
C --> D[TodoItem 1]
C --> E[TodoItem 2]
C --> F[TodoItem n]
D --> G[Toggle Complete]
D --> H[Edit Text]
D --> I[Delete]
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#9cf,stroke:#333,stroke-width:2px
style C fill:#9cf,stroke:#333,stroke-width:2px
style D fill:#9f9,stroke:#333,stroke-width:2px
Step 3: Carry Out the Plan
Project Structure
todo-app/
├── public/
│ ├── index.html
│ └── favicon.png
├── src/
│ ├── components/
│ │ ├── TodoForm.js
│ │ ├── TodoList.js
│ │ └── TodoItem.js
│ ├── App.js
│ ├── App.css
│ └── index.js
├── package.json
└── README.md
Solution 1: Basic Implementation
File: src/App.js
import React, { useState } from 'react';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import './App.css';
function App() {
// State to hold all todos
const [todos, setTodos] = useState([]);
// Create: Add new todo
const addTodo = (text) => {
// Create new todo object with unique ID
const newTodo = {
id: Date.now(), // Simple unique ID
text: text,
completed: false,
createdAt: new Date()
};
// Add new todo to state
setTodos([...todos, newTodo]);
};
// Update: Toggle todo completion
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
// Update: Edit todo text
const editTodo = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
));
};
// Delete: Remove todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="app">
<header>
<h1>My Todo List</h1>
</header>
<main>
<TodoForm onSubmit={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onEdit={editTodo}
onDelete={deleteTodo}
/>
</main>
</div>
);
}
export default App;
File: src/components/TodoForm.js
import React, { useState } from 'react';
function TodoForm({ onSubmit }) {
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// Don't submit empty todos
if (!inputValue.trim()) return;
// Call parent's submit function
onSubmit(inputValue);
// Clear input
setInputValue('');
};
return (
<form onSubmit={handleSubmit} className="todo-form">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="What needs to be done?"
className="todo-input"
/>
<button type="submit" className="add-button">
Add Todo
</button>
</form>
);
}
export default TodoForm;
File: src/components/TodoList.js
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle, onEdit, onDelete }) {
// Handle empty state
if (todos.length === 0) {
return (
<div className="empty-state">
<p>No todos yet! Add one above.</p>
</div>
);
}
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onEdit={onEdit}
onDelete={onDelete}
/>
))}
</ul>
);
}
export default TodoList;
File: src/components/TodoItem.js
import React, { useState } from 'react';
function TodoItem({ todo, onToggle, onEdit, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
if (isEditing && editText.trim() !== todo.text) {
onEdit(todo.id, editText);
}
setIsEditing(!isEditing);
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleEdit();
}
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="todo-checkbox"
/>
{isEditing ? (
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyPress={handleKeyPress}
className="edit-input"
autoFocus
/>
) : (
<span className="todo-text">{todo.text}</span>
)}
<div className="todo-actions">
<button
onClick={handleEdit}
className="edit-button"
>
{isEditing ? 'Save' : 'Edit'}
</button>
<button
onClick={() => onDelete(todo.id)}
className="delete-button"
>
Delete
</button>
</div>
</li>
);
}
export default TodoItem;
File: src/App.css
.app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
header {
text-align: center;
margin-bottom: 30px;
}
.todo-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.add-button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-button:hover {
background-color: #45a049;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #eee;
margin-bottom: 10px;
border-radius: 4px;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #888;
}
.todo-checkbox {
margin-right: 10px;
}
.todo-text {
flex: 1;
}
.edit-input {
flex: 1;
padding: 5px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.todo-actions {
display: flex;
gap: 5px;
}
.edit-button, .delete-button {
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.edit-button {
background-color: #2196F3;
color: white;
}
.delete-button {
background-color: #f44336;
color: white;
}
.empty-state {
text-align: center;
padding: 20px;
color: #666;
}
Solution 2: Advanced Implementation with Local Storage
File: src/App.js (Advanced Version)
import React, { useState, useEffect } from 'react';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import './App.css';
function App() {
// Initialize state from localStorage
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem('todos');
return savedTodos ? JSON.parse(savedTodos) : [];
});
// Save to localStorage whenever todos change
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// Add filter state
const [filter, setFilter] = useState('all'); // all, active, completed
// Filter todos based on selected filter
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
// CRUD operations remain the same...
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text: text,
completed: false,
createdAt: new Date().toISOString()
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const editTodo = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// Clear completed todos
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
// Stats
const activeTodosCount = todos.filter(todo => !todo.completed).length;
const completedTodosCount = todos.filter(todo => todo.completed).length;
return (
<div className="app">
<header>
<h1>My Todo List</h1>
<div className="stats">
<span>Active: {activeTodosCount}</span>
<span>Completed: {completedTodosCount}</span>
<span>Total: {todos.length}</span>
</div>
</header>
<main>
<TodoForm onSubmit={addTodo} />
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
Completed
</button>
</div>
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onEdit={editTodo}
onDelete={deleteTodo}
/>
{completedTodosCount > 0 && (
<button
onClick={clearCompleted}
className="clear-completed"
>
Clear Completed
</button>
)}
</main>
</div>
);
}
export default App;
Solution 3: Advanced with Custom Hooks
File: src/hooks/useTodos.js
import { useState, useEffect } from 'react';
function useTodos() {
const [todos, setTodos] = useState(() => {
const savedTodos = localStorage.getItem('todos');
return savedTodos ? JSON.parse(savedTodos) : [];
});
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text: text,
completed: false,
createdAt: new Date().toISOString()
};
setTodos(prevTodos => [...prevTodos, newTodo]);
};
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
const editTodo = (id, newText) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
)
);
};
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
const clearCompleted = () => {
setTodos(prevTodos => prevTodos.filter(todo => !todo.completed));
};
return {
todos,
addTodo,
toggleTodo,
editTodo,
deleteTodo,
clearCompleted
};
}
export default useTodos;
Step 4: Look Back and Reflect
Key Concepts Used
- Components: Modular UI building blocks
- State Management: useState for dynamic data
- Props: Passing data and callbacks between components
- Event Handling: Form submission, button clicks, keyboard events
- List Rendering: Mapping todos to components
- Conditional Rendering: Empty states, edit mode, filters
- Controlled Components: Form inputs tied to state
Real-World Applications
This todo app pattern is found in many applications:
- Task management systems (Trello, Asana)
- Shopping lists
- Note-taking apps
- Project management tools
- Bug tracking systems
Extensions You Can Add
- Due dates for todos
- Priority levels
- Categories or tags
- Search functionality
- Drag and drop reordering
- Multiple lists
- User authentication
- Cloud synchronization
Testing Your Application
Manual Testing Checklist
- ✓ Can add new todos
- ✓ Can't add empty todos
- ✓ Can mark todos as complete
- ✓ Can unmark completed todos
- ✓ Can edit todo text
- ✓ Can delete todos
- ✓ Todos persist after page refresh
- ✓ Filters work correctly
- ✓ Clear completed works
Console Testing
// Test in browser console
localStorage.setItem('todos', JSON.stringify([
{ id: 1, text: 'Test Todo', completed: false },
{ id: 2, text: 'Completed Todo', completed: true }
]));
// Refresh page - todos should load
Deployment Instructions
Option 1: GitHub Pages
# 1. Add to package.json
"homepage": "https://yourusername.github.io/todo-app",
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
}
# 2. Install gh-pages
npm install --save-dev gh-pages
# 3. Deploy
npm run deploy
Option 2: Netlify
- Push code to GitHub
- Connect repository to Netlify
- Set build command:
npm run build - Set publish directory:
build - Deploy!
Conclusion
Congratulations! You've built a fully functional Todo application using React. This project demonstrates your understanding of:
- Component-based architecture
- State management
- Event handling
- CRUD operations
- Local storage integration
- Conditional rendering
- List manipulation
This foundation will serve you well as you build more complex React applications. Keep practicing and experimenting with new features!