Build a React Todo Application with CRUD Operations

Week 4 Weekend Project: Applying Everything You've Learned

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:

Expected Input:

Expected Output:

Step 2: Devise a Plan

Whiteboard Plan:

  1. Create basic React project structure
  2. Design data model for todo items
  3. Build main App component with state
  4. Create TodoList component to display todos
  5. Create TodoItem component for individual todos
  6. Create TodoForm component for adding todos
  7. Implement CRUD operations:
    • Add todo (Create)
    • Display todos (Read)
    • Toggle completion (Update)
    • Edit todo text (Update)
    • Delete todo (Delete)
  8. 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

Real-World Applications

This todo app pattern is found in many applications:

Extensions You Can Add

Testing Your Application

Manual Testing Checklist

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

  1. Push code to GitHub
  2. Connect repository to Netlify
  3. Set build command: npm run build
  4. Set publish directory: build
  5. Deploy!

Conclusion

Congratulations! You've built a fully functional Todo application using React. This project demonstrates your understanding of:

This foundation will serve you well as you build more complex React applications. Keep practicing and experimenting with new features!