← Về danh sách bài họcBài 25/25
🚀 Bài 25: Project Cuối - Todo App Hoàn Chỉnh
🎯 Sau bài học này, bạn sẽ:
- Xây dựng Todo App hoàn chỉnh từ đầu
- Áp dụng tất cả hooks đã học
- Persist data với localStorage
- Tổng hợp kiến thức toàn khóa học
1. Setup Project
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run dev2. Todo Reducer
// hooks/useTodos.js
import { useReducer, useEffect, useCallback, useMemo } from 'react';
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, {
id: Date.now(), text: action.payload,
done: false, createdAt: new Date().toISOString()
}];
case 'TOGGLE':
return state.map(t => t.id === action.payload ? { ...t, done: !t.done } : t);
case 'DELETE':
return state.filter(t => t.id !== action.payload);
case 'EDIT':
return state.map(t => t.id === action.payload.id ? { ...t, text: action.payload.text } : t);
case 'CLEAR_DONE':
return state.filter(t => !t.done);
case 'LOAD':
return action.payload;
default: return state;
}
}
export function useTodos() {
const [todos, dispatch] = useReducer(todoReducer, [], () => {
try {
return JSON.parse(localStorage.getItem('todos')) || [];
} catch { return []; }
});
// Persist to localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = useCallback((text) =>
dispatch({ type: 'ADD', payload: text }), []);
const toggleTodo = useCallback((id) =>
dispatch({ type: 'TOGGLE', payload: id }), []);
const deleteTodo = useCallback((id) =>
dispatch({ type: 'DELETE', payload: id }), []);
const editTodo = useCallback((id, text) =>
dispatch({ type: 'EDIT', payload: { id, text } }), []);
const clearDone = useCallback(() =>
dispatch({ type: 'CLEAR_DONE' }), []);
const stats = useMemo(() => ({
total: todos.length,
done: todos.filter(t => t.done).length,
active: todos.filter(t => !t.done).length,
}), [todos]);
return { todos, addTodo, toggleTodo, deleteTodo, editTodo, clearDone, stats };
}3. Components
// components/TodoInput.jsx
import { useState, useRef, useEffect } from 'react';
export function TodoInput({ onAdd }) {
const [text, setText] = useState('');
const inputRef = useRef(null);
useEffect(() => { inputRef.current.focus(); }, []);
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
onAdd(text.trim());
setText('');
}
};
return (
<form onSubmit={handleSubmit} className="todo-input">
<input ref={inputRef} value={text}
onChange={e => setText(e.target.value)}
placeholder="Bạn cần làm gì?" />
<button type="submit" disabled={!text.trim()}>Thêm</button>
</form>
);
}
// components/TodoItem.jsx
import { memo, useState } from 'react';
export const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [editing, setEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleSave = () => {
if (editText.trim()) {
onEdit(todo.id, editText.trim());
setEditing(false);
}
};
return (
<li className={`todo-item ${todo.done ? 'done' : ''}`}>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{editing ? (
<input value={editText} onChange={e => setEditText(e.target.value)}
onBlur={handleSave} onKeyDown={e => e.key === 'Enter' && handleSave()} autoFocus />
) : (
<span onDoubleClick={() => setEditing(true)}>{todo.text}</span>
)}
<button onClick={() => onDelete(todo.id)} className="delete">✕</button>
</li>
);
});4. App Component
// App.jsx
import { useState, useMemo } from 'react';
import { useTodos } from './hooks/useTodos';
import { TodoInput } from './components/TodoInput';
import { TodoItem } from './components/TodoItem';
function App() {
const { todos, addTodo, toggleTodo, deleteTodo, editTodo, clearDone, stats } = useTodos();
const [filter, setFilter] = useState('all');
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active': return todos.filter(t => !t.done);
case 'done': return todos.filter(t => t.done);
default: return todos;
}
}, [todos, filter]);
return (
<div className="app">
<h1>📝 Todo App</h1>
<TodoInput onAdd={addTodo} />
<div className="filters">
{['all', 'active', 'done'].map(f => (
<button key={f} className={filter === f ? 'active' : ''}
onClick={() => setFilter(f)}>
{f === 'all' ? 'Tất cả' : f === 'active' ? 'Chưa xong' : 'Đã xong'}
</button>
))}
</div>
<ul className="todo-list">
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo}
onToggle={toggleTodo} onDelete={deleteTodo} onEdit={editTodo} />
))}
</ul>
<div className="stats">
<span>{stats.active} chưa xong / {stats.total} tổng</span>
{stats.done > 0 &&
<button onClick={clearDone}>Xóa {stats.done} đã xong</button>
}
</div>
</div>
);
}5. Hooks Đã Sử Dụng
| Hook | Mục Đích |
|---|---|
useReducer | Quản lý CRUD logic cho todos |
useState | Input text, filter, edit mode |
useEffect | Persist data vào localStorage |
useCallback | Memoize dispatch wrappers |
useMemo | Filter và tính statistics |
useRef | Auto-focus input |
memo | Ngăn TodoItem re-render không cần thiết |
🎉 Chúc Mừng! Bạn Đã Hoàn Thành Khóa Học!
- Bạn đã xây dựng Todo App sử dụng 7+ hooks
- Áp dụng: state management, memoization, persistence, patterns
- Tiếp theo: thêm React Router, API integration, authentication
- Nâng cao: Server Components, Next.js, React Native