项目实战:待办事项应用
让我们综合运用所学的知识,构建一个功能完整的待办事项应用。
项目结构
src/
├── components/
│ ├── TodoList.jsx
│ ├── TodoItem.jsx
│ ├── TodoForm.jsx
│ └── FilterBar.jsx
├── hooks/
│ └── useLocalStorage.js
├── App.jsx
└── main.jsx
1. 自定义 Hook:useLocalStorage
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;
2. TodoForm 组件
import { useState } from 'react';
function TodoForm({ onAdd }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
onAdd({
id: Date.now(),
text: text.trim(),
completed: false
});
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="添加新的待办事项..."
/>
<button type="submit">添加</button>
</form>
);
}
3. TodoItem 组件
import { memo } from 'react';
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
});
export default TodoItem;
4. FilterBar 组件
import { useMemo } from 'react';
function FilterBar({ filter, onFilterChange, todos }) {
const stats = useMemo(() => {
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
const active = total - completed;
return { total, completed, active };
}, [todos]);
return (
<div>
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => onFilterChange('all')}
>
全部 ({stats.total})
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => onFilterChange('active')}
>
未完成 ({stats.active})
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => onFilterChange('completed')}
>
已完成 ({stats.completed})
</button>
</div>
);
}
5. TodoList 组件
import { useMemo } from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggle, onDelete }) {
const sortedTodos = useMemo(() => {
return [...todos].sort((a, b) => {
// 未完成的排前面
if (a.completed === b.completed) {
return b.id - a.id; // 新的排前面
}
return a.completed ? 1 : -1;
});
}, [todos]);
return (
<ul>
{sortedTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
export default TodoList;
6. 主 App 组件
import { useState, useMemo, useCallback } from 'react';
import useLocalStorage from './hooks/useLocalStorage';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import FilterBar from './components/FilterBar';
function App() {
const [todos, setTodos] = useLocalStorage('todos', []);
const [filter, setFilter] = useState('all');
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]);
const handleAdd = useCallback((todo) => {
setTodos(prev => [...prev, todo]);
}, [setTodos]);
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, [setTodos]);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, [setTodos]);
const handleClearCompleted = useCallback(() => {
setTodos(prev => prev.filter(todo => !todo.completed));
}, [setTodos]);
return (
<div>
<h1>待办事项</h1>
<TodoForm onAdd={handleAdd} />
<FilterBar
filter={filter}
onFilterChange={setFilter}
todos={todos}
/>
<TodoList
todos={filteredTodos}
onToggle={handleToggle}
onDelete={handleDelete}
/>
{todos.some(t => t.completed) && (
<button onClick={handleClearCompleted}>
清除已完成
</button>
)}
</div>
);
}
export default App;
样式
// App.css
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
form input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.active {
background: #007bff;
}
💡 项目总结
通过这个项目,你学会了:
- ✅ 使用自定义 Hook 管理本地存储
- ✅ 使用 React.memo 优化列表渲染
- ✅ 使用 useMemo 缓存计算结果
- ✅ 使用 useCallback 避免不必要的函数重建
- ✅ 组件化和状态管理
- ✅ 表单处理和事件处理
🎯 下一步学习
恭喜你完成了 React 快速入门!接下来可以:
- 学习 React Router 实现路由
- 学习 状态管理库(Redux、Zustand)
- 学习 React Query 处理服务端状态
- 学习 Next.js 构建全栈应用
- 探索 UI 组件库(Material-UI、Ant Design)
继续保持学习的热情,React 的世界很精彩!