今天在React世界里捣鼓了一个有趣的待办事项应用,收获颇丰!整个过程就像在组装一台精密的通讯设备:组件之间要传递消息,数据需要持久保存,还要确保各个部件协调工作。下面让我用幽默的方式带你回顾这段奇妙旅程。
最终我们实现的效果:

那么我们是如何实现这个任务列表的功能的呢?
一、组件家族:各司其职的奇妙团队
想象一下我们的应用就像一个家庭聚会:
-
App组件:佛系家长
javascriptfunction App() { return ( <> <Todos /> </> ) }
这位家长深谙"无为而治"之道,直接把所有家务丢给了Todos组件,自己躺平喝咖啡(注释掉的那段CSS实验品就是它的休闲时光)。
-
Todos组件:家庭总管
iniconst Todos = () => { const { todos, addTodo, onToggle, onDelete } = useTodos() return ( <div className='app'> <TodoForm onAddTodo={addTodo} /> <TodoList Todos={todos} onToggle={onToggle} onDelete={onDelete}/> </div> ) }
总管大人手握核心机密(useTodos钩子),把任务分发给两个得力助手:表单管家和列表管家。
-
TodoForm组件:家庭秘书
javascriptconst TodoForm = ({ onAddTodo }) => { const [text, setText] = useState('') const handleSubmit = (e) => { e.preventDefault() onAddTodo(text.trim()) setText('') } return ( <form onSubmit={handleSubmit}> <input value={text} onChange={e => setText(e.target.value)} /> <button type='submit'>Add</button> </form> ) }
这位秘书有强迫症:输入必须.trim()去空格,提交后必须清空输入框。它的口头禅是:"数据状态和界面状态必须保持绝对一致!"
-
TodoList组件:任务分发员
iniconst TodoList = ({ Todos, onToggle, onDelete }) => ( <ul className="todo-list"> {Todos.length > 0 ? Todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={() => onToggle(todo.id)} onDelete={() => onDelete(todo.id)}/> )) : <p>暂无代办事项</p> } </ul> )
它像圣诞老人一样,遍历所有待办事项,给每个任务打包好专属ID礼物再分发给TodoItem。
-
TodoItem组件:一线执行者
ini
const TodoItem = ({ todo, onToggle, onDelete }) => (
<div className="todo-item">
<input type="checkbox" checked={todo.isComplete} onChange={onToggle}/>
<span className={todo.isComplete ? 'completed' : ''}>{todo.text}</span>
<button onClick={onDelete}>Delete</button>
</div>
)
这位执行者最擅长变脸:任务完成时给文字加上.completed类名,就像给完成任务戴上胜利勋章。
在react中我们使用组件化的开发方式,就像拼积木一样,将开发的好的功能一个个拼接在一起,方便我们的复用
二、组件通讯:React世界的传纸条艺术
在React组件间传递数据就像学生时代传纸条:
-
父子通讯(向下传递) :爸爸给零花钱
ini// 父组件Todos给子组件TodoForm发"零花钱" <TodoForm onAddTodo={addTodo} /> // 子组件TodoForm开心收下 const TodoForm = ({ onAddTodo }) => { ... }
-
子父通讯(向上传递) :孩子要买玩具
scss// 子组件提交时"撒娇" const handleSubmit = (e) => { e.preventDefault() onAddTodo(text.trim()) // "爸,我想买这个!" } // 父组件慷慨回应 addTodo = (text) => { setTodos([...todos, { id: Date.now(), text, isComplete: false }]) }
-
兄弟通讯(间接传递) :通过父母中转
ini// 哥哥TodoList向弟弟TodoItem传话 <TodoItem onToggle={() => onToggle(todo.id)}/> // 弟弟收到后执行操作 <input type="checkbox" onChange={onToggle}/>
这就像哥哥对弟弟说:"爸妈同意你吃糖了",弟弟才敢行动。
这种通信方式非常繁琐,需要一层层汇报,之后会有
useContext
来更好的实现跨层级的组件通信,以及常用的Rudux状态集中管理工具,这里也是本项目未实现功能的遗憾 -
ID护照制度:精准定位的秘诀
scss// 给每个任务发护照 { id: Date.now(), text, isComplete: false } // 出入境检查 onToggle={() => onToggle(todo.id)} onDelete={() => onDelete(todo.id)}
有了ID这个专属护照,我们才能精准找到要操作的任务,避免"误伤无辜"。
三、useTodos钩子:数据保险箱
这个自定义Hook堪称我们的应用大脑,实现了两大魔法:
魔法一:本地存储持久化
javascript
export const useTodos = () => {
// 从保险箱读取数据
const [todos, setTodos] = useState(
JSON.parse(localStorage.getItem('todos'))
)
// 数据变化时自动存回保险箱
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos))
}, [todos])
// ...其他操作
}
这个设计太贴心了!就像有个小精灵在你修改待办事项时自动帮你保存:
- 刷新页面?数据还在!
- 关闭浏览器?数据还在!
- 不小心踢掉电源?数据还在!
这里我简单的来演示一下使用本地存储和不使用本地存储的区别
const
const ul = document.getElementById('ul');
function fn() {
ul.innerHTML = list.map(item => `<li>${item}</li>`).join('')
}
fn();
const input = document.getElementById('input');
const button = document.getElementById('button');
button.addEventListener('click', () => {
if (input.value) {
list.push(input.value);
} else {
alert('请输入内容')
}
fn();
})
当我们不使用本地存储,我们的数据是死数据,我们添加的树会随着页面的刷新也丢失,就如同下面的效果

javascript
// 获取 ul 元素
const ul = document.getElementById('ul');
// 获取 input 和 button 元素
const input = document.getElementById('input');
const button = document.getElementById('button');
// 从 localStorage 获取列表数据,若没有则初始化为默认值
function getList() {
const storedList = localStorage.getItem('fruitList');
return storedList ? JSON.parse(storedList) : ['苹果', '香蕉', '橘子', '西瓜'];
}
// 渲染列表到页面
function renderList() {
const list = getList();
ul.innerHTML = list.map(item => `<li>${item}</li>`).join('');
}
// 初始渲染
renderList();
// 添加点击事件
button.addEventListener('click', () => {
const value = input.value.trim();
if (value) {
let list = getList();
list.push(value);
localStorage.setItem('fruitList', JSON.stringify(list)); // 保存回 localStorage
renderList(); // 更新视图
input.value = ''; // 清空输入框
} else {
alert('请输入内容');
}
});
当我们使用localStorage
我们的数据会被保存到本地,刷新后也不会丢失

我们的localStorage存储的数据是存放在浏览器的,可以看到图中存放的数据是有地址区分的,不同的网页,有不同的本地存储的数据

之前我们写过一篇文章介绍了Cookie,http是无状态的,而我们的Cookie可以实现身份识别,这样我们所发送的Http请求就好像是带有了状态
,其实这一切都是Cookie的功劳 大家感兴趣可以看看这篇文章当饼干遇上代码:一场HTTP与Cookie的奇幻漂流 🍪🌊
这里我们简单的说一下两者的区别
特性 | localStorage | Cookie |
---|---|---|
存储容量 | 通常为5MB或更多,具体取决于浏览器。 | 通常每个域名4KB(包括所有元数据),实际可用空间更小。 |
有效期 | 数据没有过期时间,除非用户手动清除或者通过代码清除。 | 可以设置过期时间;如果不设置,默认是会话级别的,浏览器关闭后即失效。 |
传输 | 数据仅在客户端使用,不会自动发送到服务器。 | 每次HTTP请求都会将cookie发送给服务器(如果cookie的path和domain匹配)。 |
访问范围 | 同源策略下,所有同源窗口共享相同的数据。 | 同源策略下,所有同源窗口共享相同的数据。 |
安全性 | 支持通过设置StorageManager 进行管理,但本身不提供加密等安全措施。 |
支持Secure 标志,确保只通过HTTPS发送;支持HttpOnly 标志,防止JavaScript访问。 |
操作便捷性 | 使用简单,主要通过setItem , getItem , removeItem 等方法操作。 |
设置相对复杂一些,需要指定更多的参数如路径、过期时间等。 |
适用场景 | 适合存储大量不敏感的数据,例如用户偏好设置、应用状态等。 | 适合存储少量用于服务器验证的数据,例如会话ID、跟踪信息等。 |
API易用性 | 提供了简单直接的API,易于上手。 | API相对较老,不如现代Web API直观,且有较多细节需要注意。 |
魔法二:任务管理三连击
ini
// 添加任务:生成专属ID
const addTodo = (text) => {
setTodos([
...todos,
{ id: Date.now(), text, isComplete: false }
])
}
// 切换状态:精准定位
const onToggle = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, isComplete: !todo.isComplete }
: todo
))
}
// 删除任务:过滤大法
const onDelete = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
这三个操作展示了React不变性原则的精髓------永远不用直接修改状态,而是创建新副本。就像复印文件时修改复印件,原件永远安全!
四、关键技巧:React开发的武功秘籍
-
状态提升的艺术
scss// 在Todos组件统一管理状态 const { todos, addTodo, onToggle, onDelete } = useTodos()
把状态放在共同祖先组件,就像把家庭相册放在客厅而不是某个卧室,全家人都能访问。
-
闭包传参妙招
ini// 在TodoList中 onToggle={() => onToggle(todo.id)} // 代替 onToggle={onToggle} // 这样会丢失id信息!
使用箭头函数包裹调用,就像给消息加上收件人地址,确保准确送达。
-
条件渲染的优雅表达
javascript{Todos.length > 0 ? Todos.map(...) : <p>暂无代办事项</p> }
比if/else更优雅,像诗人写代码:"若有任务,则列之;若无,则示空"。
-
样式切换的妙用
css<span className={isComplete ? 'completed' : ''}> {text} </span>
完成任务时添加completed类名,就像给任务戴上小红花,视觉反馈让用户成就感满满。
五、踩坑警示:那些年我跳过的坑
-
初始状态陷阱
scss// 错误示范 const [todos, setTodos] = useState([]) // 正确姿势 const [todos, setTodos] = useState( JSON.parse(localStorage.getItem('todos') || [] )
如果不考虑localStorage初始为空的情况,程序会崩溃,就像开保险箱忘记初始密码。
-
ID冲突危机
css{ id: Date.now(), ... }
用时间戳做ID在99.99%情况下安全,但如果用户手速超光速(1毫秒内添加多个任务),可能产生冲突。生产环境建议使用uuid库。
-
直接修改状态的禁忌
ini// 大忌! const onToggle = (id) => { todos.find(todo => todo.id === id).isComplete = true setTodos(todos) }
React状态必须视为不可变数据,直接修改就像在博物馆名画上涂改------后果很严重!相应的数据只能通过hook函数来修改
六、总结:React开发的智慧
- 组件设计哲学:单一职责原则,每个组件只做一件事
- 数据流动规范:单向数据流,避免数据混乱
- 状态管理智慧:合理提升状态,避免"道具钻探"
- 副作用处理:useEffect处理副作用,保持纯净渲染
- 持久化策略:善用localStorage,提升用户体验
这个待办事项应用虽小,却涵盖了React开发的精髓。就像乐高积木,简单组件组合出强大功能。最后分享我的编码心得:"好的React组件就像好笑话------不需要解释就能工作!"
现在每当我看到localStorage默默保存数据,就想起那句名言:"在数字世界里,唯有localStorage的爱是永恒的..."(刷新页面也不会消失!)