从 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 组件通信,直接把这个待办项目甩给他看 ——「喏,都在这儿了~」😎