从 React Todos 中 学习组件通信机制 🎯
嗨,各位前端小伙伴~ 今天咱们不聊虚的,直接拿一个实实在在的「React 待办清单」项目开刀,聊聊 React 里最核心的组件通信那些事儿。毕竟,学 React 不学组件通信,就像学做饭不学开火 ------ 根本玩不转啊!
一、项目介绍 📝
先给大家亮亮家底,这个「React Todos」项目别看小,五脏俱全:
- 能添加待办事项(比如「今晚打游戏」、「明天写博客」)
- 能勾选完成状态(打勾的那一刻,成就感爆棚有没有!)
- 能删除不需要的待办(手滑写错了?删就完事儿了~)
- 能统计总数、未完成和已完成数量(清清楚楚,明明白白)
- 能一键清空已完成(清理战场,清爽!)
- 还能把数据存在本地(刷新页面?重启浏览器?待办还在,安全感拉满!)
技术嘛,也是当下流行的:vite + react + stylus。vite 负责快速启动和热更新(再也不用等 webpack 慢悠悠打包了😭),react 负责组件化和状态管理,stylus 让写 CSS 像写代码一样爽(不用写大括号和分号,懒人福音!)。
二、准备工作 🛠️
想亲手试试这个项目?安排!步骤简单到不行:
- 打开终端,敲
npm init vite(vite 脚手架,快得飞起) - 给项目起个名,比如
todos(简单直接,好记) - 框架选
react(咱们今天的主角) - 语言选
javascript(基础易上手) - 进入项目目录,执行
npm i安装依赖(等着它跑完就行,喝口水的功夫) - 最后
npm run dev启动项目,齐活!
三、从三个角度吃透组件通信 🔍
在 React 里,组件就像一个个独立的小零件,要让它们协同工作,就得靠「通信」。而通信的核心,其实就是「数据」的传递和修改 ------ 毕竟组件们忙活半天,本质上都是在跟数据打交道。
先看根组件 App.jsx 里的状态定义:
javascript
运行
// App.jsx
const [todos, setTodos] = useState(() => {
// 从localStorage读取数据,实现刷新不丢失
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
这里的 todos 就是整个应用的「核心数据」,所有组件的通信几乎都是围绕它展开的。就像一个家庭的「共用冰箱」,食材(数据)都存在这里,全家人(组件)都要靠它吃饭~
1. 父组件 → 子组件:我给你啥,你就用啥 📦
在咱们的项目里,App 是根组件(大老板),TodoInput、TodoList、TodoStats 都是它的子组件(小员工)。父组件给子组件传数据,靠的是「props」这个神奇的东西。
举个栗子 🌰
比如 App 给 TodoList 传数据:
// App.jsx 中使用 TodoList
<TodoList
todos={todos} // 传递待办列表数据
onDelete={deleteTodo} // 传递删除方法
onToggle={tooggleTodo} // 传递切换状态方法
/>
子组件 TodoList 接收并使用这些 props:
// TodoList.jsx
const TodoList = (props) => {
// 从props中解构出需要的东西
const { todos, onDelete, onToggle } = props;
return (
<ul className="todo-list">
{todos.map(todo => ( // 直接使用todos数据渲染列表
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<label>
<input
type="checkbox"
checked={todo.completed} // 使用todo的completed状态
onChange={() => onToggle(todo.id)} // 调用父组件传的onToggle方法
/>
<span>{todo.text}</span> // 使用todo的文本内容
</label>
<button onClick={() => onDelete(todo.id)}>X</button> // 调用父组件传的onDelete方法
</li>
))}
</ul>
)
}
本质揭秘 🕵️
父组件通过 props 给子组件传的「值」可不止是数据,还能是方法、甚至其他组件!就像爸爸给孩子零花钱(数据)、给孩子一把家门钥匙(方法,用来开门 / 修改数据)------ 孩子能花钱、能开门,但不能直接改爸爸的工资卡(props 是只读的!)。
React 严格遵循「单向数据流」:数据从父到子,一层一层往下传。子组件只能用 props,不能直接改 props。这样做的好处是「数据流向可追踪」,出了问题能快速定位 ------ 就像快递物流,从卖家(父)到买家(子),每一步都有记录,丢了件也好查~
2. 子组件 → 父组件:有事您说话,我喊您处理 📣
子组件不能直接改父组件的数据(单向数据流规定的!),那子组件想修改数据咋办?比如 TodoInput 输入了新的待办内容,总不能自己偷偷加到 todos 里吧~
这时候就得用「回调函数」大法了:父组件提前把「修改数据的方法」通过 props 传给子组件,子组件需要修改时,调用这个方法就行。
举个栗子 🌰
父组件 App 定义添加待办的方法,并传给 TodoInput:
// App.jsx
const addTodo = (text) => {
// 往todos里加新待办
setTodos([...todos, {
id: Date.now(), // 用时间戳当唯一ID,简单粗暴
text: text,
completed: false // 刚添加的肯定是未完成状态
}]);
};
// 传给子组件 TodoInput
<TodoInput onAdd={addTodo} />
子组件 TodoInput 接收方法,在合适的时机调用:
// TodoInput.jsx
const TodoInput = (props) => {
const { onAdd } = props; // 接收父组件的onAdd方法
const [inputValue, setInputValue] = useState(''); // 本地维护输入框状态
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认提交
if (inputValue.trim() === '') return; // 空内容不提交,避免无效数据
onAdd(inputValue); // 调用父组件的方法,把输入的文本传过去
setInputValue(''); // 清空输入框,用户体验up
}
return (
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={e => setInputValue(e.target.value)} // 实时更新输入框状态
/>
<button type="submit">Add</button>
</form>
)
}
本质揭秘 🕵️
子传父的核心就是「父给方法,子调方法传数据」。就像孩子想要买玩具(修改数据),不能直接从爸爸钱包里拿钱(改父组件状态),但可以跟爸爸说「我想要这个玩具」(调用回调函数传数据),爸爸听到后,自己掏钱买(父组件自己修改状态)。既满足了需求,又遵守了「规矩」~
3. 兄弟组件通信:有事找爸爸转达 👨👩👧👦
TodoInput、TodoList、TodoStats 这三个组件是「兄弟关系」------ 它们的爸爸都是 App。兄弟之间想通信咋办?比如 TodoInput 新增了一个待办,TodoList 要显示新内容,TodoStats 要更新统计数字。
React 里兄弟组件不能直接聊天,得靠「爸爸当中间人」:结合「子传父」和「父传子」,让爸爸来转发消息。
举个栗子 🌰
- 爸爸(
App)持有共享数据todos和修改方法(addTodo、deleteTodo等); TodoInput(哥哥)通过onAdd把新待办传给爸爸(子传父);- 爸爸更新
todos状态; - 爸爸把最新的
todos传给TodoList(弟弟)和TodoStats(妹妹)(父传子); - 弟弟和妹妹拿到新数据,重新渲染,实现了「间接通信」。
看 TodoStats 组件的代码就明白了:
// TodoStats.jsx
const TodoStats = (props) => {
const { todos, active, completed, onClearCompleted } = props;
return (
<div className="todo-stats">
<p>Total: {todos} | Active: {active} | Completed: {completed}</p>
{completed > 0 && (
<button className="clear-btn" onClick={onClearCompleted}>
Clear Completed
</button>
)}
</div>
)
}
它展示的 todos 总数、active 未完成数、completed 已完成数,都是爸爸 App 计算好传过来的。当 TodoList 里勾选了一个待办(调用 onToggle 改了 todos),爸爸会重新计算 active 和 completed,然后传给 TodoStats,于是统计数字就自动更新了 ------ 这就是兄弟通信的精髓!
本质揭秘 🕵️
兄弟通信就像两个小朋友隔着房间聊天:哥哥(TodoInput)喊爸爸(App):「我放了个苹果在冰箱里!」,爸爸听到后更新冰箱(todos),然后告诉妹妹(TodoStats):「冰箱里多了个苹果,现在总数是 5 个啦~」。虽然兄弟没直接说话,但靠爸爸转达,信息照样同步~
四、数据持久化:localStorage 来帮忙 💾
咱们的待办列表,刷新页面后数据还在,这是咋做到的?秘密就在 localStorage------ 浏览器提供的本地存储功能,能把数据存在用户的电脑里,关掉浏览器也不丢。
1.不好的做法 ❌
很多新手可能会想到:在每个修改 todos 的方法里都手动存一次数据。比如:
// 不好的示例:重复代码太多!
const addTodo = (text) => {
const newTodos = [...todos, { id: Date.now(), text, completed: false }];
setTodos(newTodos);
localStorage.setItem('todos', JSON.stringify(newTodos)); // 手动存
};
const deleteTodo = (id) => {
const newTodos = todos.filter(todo => todo.id !== id);
setTodos(newTodos);
localStorage.setItem('todos', JSON.stringify(newTodos)); // 又存一次
};
这写法的缺点太明显了:重复代码多(每次改数据都要写一遍 localStorage.setItem)、容易漏(万一新增了一个修改方法忘了存,数据就丢了)。简直就像每次吃完零食都要手动写一遍账本,麻烦还容易错!
2.聪明的做法 ✅
用 React 的 useEffect 钩子!它能监听 todos 的变化,只要 todos 变了,就自动存到 localStorage 里。一次写好,终身受益~
// App.jsx
useEffect(() => {
// 当todos变化时,自动存到localStorage
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]); // 依赖数组:只有todos变了,才会执行上面的代码
useEffect第一个参数是「副作用函数」(这里就是存数据的操作);- 第二个参数
[todos]是「依赖数组」:只有当todos发生变化时,才会执行副作用函数。
这就像给冰箱装了个自动记账器 ------ 不管是加了苹果(addTodo)、扔了香蕉(deleteTodo),还是把草莓标成「已吃」(onToggle),只要冰箱里的东西变了,记账器就自动更新账本(localStorage),省心!
效果展示:

五、面试官可能会问这些 🤔
学完这个项目,面试官再问 React 组件通信,你就可以自信回答了:
- React 单向数据流有啥好处? 答:数据流向清晰(父→子),容易调试(知道谁改了数据),避免数据混乱(子组件不能乱改父组件数据)。就像咱们项目里,所有
todos的修改都集中在App里,出问题一查就准~ - 子组件想改父组件数据咋办? 答:父组件定义回调函数,通过 props 传给子组件,子组件调用函数传数据。比如
TodoInput用onAdd给App传新待办内容~ - 兄弟组件咋通信? 答:通过父组件中转!父组件存共享状态,一个子组件改状态(子传父),父组件把新状态传给另一个子组件(父传子)。就像
TodoInput新增待办,TodoList和TodoStats自动更新~ - useEffect 在项目里用在哪了?作用是啥? 答:用来监听
todos变化,自动同步到localStorage,实现数据持久化。依赖数组[todos]保证了只有数据变了才会执行,避免无效操作~
六、项目结构及效果展示 📑
整个项目的代码结构特别清晰:
App.jsx:核心组件,管着todos状态和所有修改方法(addTodo、deleteTodo等),负责给子组件传数据和方法;TodoInput.jsx:负责输入新待办,通过onAdd告诉爸爸;TodoList.jsx:负责展示待办列表,通过onDelete和onToggle告诉爸爸要删还是要改;TodoStats.jsx:负责展示统计信息,通过onClearCompleted告诉爸爸要清空已完成。
每个组件各司其职,靠通信协同工作,完美体现了 React 组件化的思想~
- 奥!还有我们的
app.styl美化我们的界面。
项目结构:

效果亮个相吧:

七、结语 🎉
看完这篇博客,是不是觉得 React 组件通信也没那么难?其实核心就三点:父传子靠 props,子传父靠回调,兄弟通信靠爸爸中转。再加上 useEffect 管理副作用(比如存数据),一个小而美的 React 应用就成了~
记住:多写代码多实践,遇到问题看看组件之间的数据是咋传的,慢慢就会有感觉啦!下次再有人问你 React 组件通信,直接把这个待办项目甩给他看 ------「喏,都在这儿了~」😎