Solution: React + 3 Essential Hooks
Try the Complete App
Section titled “Try the Complete App”ลองเล่น React Todo List ที่สมบูรณ์แล้ว — ลองเพิ่ม todo, filter ด้วย All/Active/Completed, และลบ todo ดู
React Todo
2 items remaining
React Todo List — Complete Solution
Section titled “React Todo List — Complete Solution”โปรเจกต์นี้ประกอบด้วย 4 ไฟล์หลัก:
- App.jsx — Component หลัก เก็บ state ทั้งหมด + useEffect (localStorage) + useMemo (filter)
- TodoForm.jsx — Form สำหรับเพิ่ม todo ใหม่
- TodoItem.jsx — แสดง todo แต่ละตัว พร้อม toggle + delete
- FilterBar.jsx — ปุ่ม filter (All / Active / Completed)
App.jsx
Section titled “App.jsx”Show App.jsx
import { useState, useEffect, useMemo } from "react";import TodoForm from "./components/TodoForm";import TodoItem from "./components/TodoItem";import FilterBar from "./components/FilterBar";
function App() { // === useState: จัดการ state ทั้งหมด === const [todos, setTodos] = useState([]); const [filter, setFilter] = useState("all"); // "all" | "active" | "completed"
// === useEffect #1: อ่าน todos จาก localStorage ตอน mount === useEffect(() => { try { const saved = localStorage.getItem("react-todos"); if (saved) { setTodos(JSON.parse(saved)); } } catch (err) { console.error("Failed to load todos:", err); } }, []); // [] = ทำครั้งเดียวตอน component โหลด
// === useEffect #2: บันทึก todos ลง localStorage ทุกครั้งที่เปลี่ยน === useEffect(() => { localStorage.setItem("react-todos", JSON.stringify(todos)); }, [todos]); // [todos] = ทำเมื่อ todos เปลี่ยน
// === useMemo: filter todos ตาม filter state === const filteredTodos = useMemo(() => { switch (filter) { case "active": return todos.filter((t) => !t.completed); case "completed": return todos.filter((t) => t.completed); default: return todos; } }, [todos, filter]);
// === useMemo: นับจำนวน todo ที่ยังไม่เสร็จ === const remainingCount = useMemo(() => { return todos.filter((t) => !t.completed).length; }, [todos]);
// === Handler functions === const addTodo = (text) => { if (!text.trim()) return; setTodos([...todos, { id: Date.now(), text: text.trim(), completed: false }]); };
const toggleTodo = (id) => { setTodos( todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)) ); };
const deleteTodo = (id) => { setTodos(todos.filter((t) => t.id !== id)); };
return ( <div className="min-h-screen bg-gray-50 py-8 px-4"> <div className="max-w-lg mx-auto"> {/* Header */} <div className="text-center mb-8"> <h1 className="text-3xl font-bold text-gray-900">React Todo</h1> <p className="text-gray-500 mt-1"> {remainingCount} {remainingCount === 1 ? "task" : "tasks"} remaining </p> </div>
{/* Todo Form */} <TodoForm onAddTodo={addTodo} />
{/* Filter Bar */} <FilterBar filter={filter} onFilterChange={setFilter} />
{/* Todo List */} <div className="space-y-2"> {filteredTodos.length === 0 ? ( <p className="text-center text-gray-400 py-8"> {filter === "all" ? "No todos yet. Add one above!" : `No ${filter} todos.`} </p> ) : ( filteredTodos.map((todo) => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} onDelete={deleteTodo} /> )) )} </div> </div> </div> );}
export default App;จุดสำคัญ:
useState2 ตัว:todos(array) และfilter(string)useEffect2 ตัว: อ่าน localStorage ตอน mount + บันทึกทุกครั้งที่ todos เปลี่ยนuseMemo2 ตัว: filter todos ตาม filter state + นับ remaining count- Handler functions (
addTodo,toggleTodo,deleteTodo) สร้าง array ใหม่ทุกครั้ง ไม่ mutate state
TodoForm.jsx
Section titled “TodoForm.jsx”Show TodoForm.jsx
import { useState } from "react";
function TodoForm({ onAddTodo }) { const [input, setInput] = useState("");
const handleSubmit = (e) => { e.preventDefault(); if (!input.trim()) return; onAddTodo(input); setInput(""); };
return ( <form onSubmit={handleSubmit} className="flex gap-2 mb-6"> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="What needs to be done?" className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-500 focus:border-transparent bg-white text-gray-900 placeholder-gray-400" /> <button type="submit" className="px-6 py-2.5 bg-pink-600 text-white font-medium rounded-lg hover:bg-pink-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled={!input.trim()} > Add </button> </form> );}
export default TodoForm;จุดสำคัญ:
inputstate อยู่ใน TodoForm เอง (ไม่ต้อง lift up เพราะ App ไม่ต้องใช้)onAddTodoเป็น callback prop ที่ส่งจาก App- ใช้
form+onSubmitเพื่อรองรับทั้งปุ่ม Add และกด Enter e.preventDefault()ป้องกัน page reload
TodoItem.jsx
Section titled “TodoItem.jsx”Show TodoItem.jsx
function TodoItem({ todo, onToggle, onDelete }) { return ( <div className="flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-200 hover:border-gray-300 transition-colors group"> {/* Checkbox */} <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} className="w-5 h-5 rounded border-gray-300 text-pink-600 focus:ring-pink-500 cursor-pointer" />
{/* Todo text */} <span className={`flex-1 text-sm transition-all ${ todo.completed ? "line-through text-gray-400" : "text-gray-700" }`} > {todo.text} </span>
{/* Delete button — shows on hover */} <button onClick={() => onDelete(todo.id)} className="text-gray-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100" aria-label={`Delete "${todo.text}"`} > <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} > <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> </svg> </button> </div> );}
export default TodoItem;จุดสำคัญ:
- ไม่มี state ของตัวเอง — เป็น “presentational component” ล้วนๆ
- รับ
todoobject + callback functions เป็น props - ใช้
group+group-hover:opacity-100ของ Tailwind เพื่อแสดงปุ่ม delete ตอน hover aria-labelสำหรับ accessibility
FilterBar.jsx
Section titled “FilterBar.jsx”Show FilterBar.jsx
const FILTERS = [ { key: "all", label: "All" }, { key: "active", label: "Active" }, { key: "completed", label: "Completed" },];
function FilterBar({ filter, onFilterChange }) { return ( <div className="flex gap-1 mb-4 p-1 bg-gray-100 rounded-lg"> {FILTERS.map((f) => ( <button key={f.key} onClick={() => onFilterChange(f.key)} className={`flex-1 py-1.5 px-3 text-sm font-medium rounded-md transition-all ${ filter === f.key ? "bg-white text-pink-600 shadow-sm" : "text-gray-500 hover:text-gray-700" }`} > {f.label} </button> ))} </div> );}
export default FilterBar;จุดสำคัญ:
FILTERSarray อยู่นอก component เพราะไม่เปลี่ยนแปลง (ไม่ต้อง re-create ทุก render)- ใช้
.map()สร้างปุ่มจาก array แทนเขียนซ้ำ 3 ครั้ง - Active filter ได้ style ต่างจากตัวอื่น (bg-white + shadow)
- ไม่มี state ของตัวเอง — ใช้
filter+onFilterChangeจาก App
สรุป Hooks ที่ใช้ในโปรเจกต์
Section titled “สรุป Hooks ที่ใช้ในโปรเจกต์”| Hook | ใช้ที่ไหน | ทำอะไร |
|---|---|---|
useState | App.jsx | เก็บ todos array + filter string |
useState | TodoForm.jsx | เก็บ input text สำหรับ form |
useEffect | App.jsx | อ่าน todos จาก localStorage ตอน mount |
useEffect | App.jsx | บันทึก todos ลง localStorage เมื่อเปลี่ยน |
useMemo | App.jsx | Filter todos ตาม filter state |
useMemo | App.jsx | นับจำนวน todo ที่ยังไม่เสร็จ |