news 2026/3/14 9:58:07

单向数据流不迷路:用 Todos 项目吃透 React 通信机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单向数据流不迷路:用 Todos 项目吃透 React 通信机制

从 React Todos 中 学习组件通信机制 🎯

嗨,各位前端小伙伴~ 今天咱们不聊虚的,直接拿一个实实在在的「React 待办清单」项目开刀,聊聊 React 里最核心的组件通信那些事儿。毕竟,学 React 不学组件通信,就像学做饭不学开火 —— 根本玩不转啊!

一、项目介绍 📝

先给大家亮亮家底,这个「React Todos」项目别看小,五脏俱全:

  • 能添加待办事项(比如「今晚打游戏」、「明天写博客」)
  • 能勾选完成状态(打勾的那一刻,成就感爆棚有没有!)
  • 能删除不需要的待办(手滑写错了?删就完事儿了~)
  • 能统计总数、未完成和已完成数量(清清楚楚,明明白白)
  • 能一键清空已完成(清理战场,清爽!)
  • 还能把数据存在本地(刷新页面?重启浏览器?待办还在,安全感拉满!)

技术嘛,也是当下流行的:vite + react + stylus。vite 负责快速启动和热更新(再也不用等 webpack 慢悠悠打包了😭),react 负责组件化和状态管理,stylus 让写 CSS 像写代码一样爽(不用写大括号和分号,懒人福音!)。

二、准备工作 🛠️

想亲手试试这个项目?安排!步骤简单到不行:

  1. 打开终端,敲npm init vite(vite 脚手架,快得飞起)
  2. 给项目起个名,比如todos(简单直接,好记)
  3. 框架选react(咱们今天的主角)
  4. 语言选javascript(基础易上手)
  5. 进入项目目录,执行npm i安装依赖(等着它跑完就行,喝口水的功夫)
  6. 最后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是根组件(大老板),TodoInputTodoListTodoStats都是它的子组件(小员工)。父组件给子组件传数据,靠的是「props」这个神奇的东西。

举个栗子 🌰

比如AppTodoList传数据:

// 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. 兄弟组件通信:有事找爸爸转达 👨‍👩‍👧‍👦

TodoInputTodoListTodoStats这三个组件是「兄弟关系」—— 它们的爸爸都是App。兄弟之间想通信咋办?比如TodoInput新增了一个待办,TodoList要显示新内容,TodoStats要更新统计数字。

React 里兄弟组件不能直接聊天,得靠「爸爸当中间人」:结合「子传父」和「父传子」,让爸爸来转发消息。

举个栗子 🌰
  1. 爸爸(App)持有共享数据todos和修改方法(addTododeleteTodo等);
  2. TodoInput(哥哥)通过onAdd把新待办传给爸爸(子传父);
  3. 爸爸更新todos状态;
  4. 爸爸把最新的todos传给TodoList(弟弟)和TodoStats(妹妹)(父传子);
  5. 弟弟和妹妹拿到新数据,重新渲染,实现了「间接通信」。

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),爸爸会重新计算activecompleted,然后传给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 组件通信,你就可以自信回答了:

  1. React 单向数据流有啥好处?答:数据流向清晰(父→子),容易调试(知道谁改了数据),避免数据混乱(子组件不能乱改父组件数据)。就像咱们项目里,所有todos的修改都集中在App里,出问题一查就准~
  2. 子组件想改父组件数据咋办?答:父组件定义回调函数,通过 props 传给子组件,子组件调用函数传数据。比如TodoInputonAddApp传新待办内容~
  3. 兄弟组件咋通信?答:通过父组件中转!父组件存共享状态,一个子组件改状态(子传父),父组件把新状态传给另一个子组件(父传子)。就像TodoInput新增待办,TodoListTodoStats自动更新~
  4. useEffect 在项目里用在哪了?作用是啥?答:用来监听todos变化,自动同步到localStorage,实现数据持久化。依赖数组[todos]保证了只有数据变了才会执行,避免无效操作~

六、项目结构及效果展示 📑

整个项目的代码结构特别清晰:

  • App.jsx:核心组件,管着todos状态和所有修改方法(addTododeleteTodo等),负责给子组件传数据和方法;
  • TodoInput.jsx:负责输入新待办,通过onAdd告诉爸爸;
  • TodoList.jsx:负责展示待办列表,通过onDeleteonToggle告诉爸爸要删还是要改;
  • TodoStats.jsx:负责展示统计信息,通过onClearCompleted告诉爸爸要清空已完成。

每个组件各司其职,靠通信协同工作,完美体现了 React 组件化的思想~

  • 奥!还有我们的app.styl美化我们的界面。

项目结构:

效果亮个相吧:

七、结语 🎉

看完这篇博客,是不是觉得 React 组件通信也没那么难?其实核心就三点:父传子靠 props,子传父靠回调,兄弟通信靠爸爸中转。再加上useEffect管理副作用(比如存数据),一个小而美的 React 应用就成了~

记住:多写代码多实践,遇到问题看看组件之间的数据是咋传的,慢慢就会有感觉啦!下次再有人问你 React 组件通信,直接把这个待办项目甩给他看 ——「喏,都在这儿了~」😎

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/11 21:58:09

PoE Overlay终极指南:从新手到高手的快速上手技巧

还在为《流放之路》中复杂的物品鉴定和繁琐的交易流程而头疼吗&#xff1f;每次打到稀有装备都要反复查询市场价&#xff0c;处理交易请求时手忙脚乱&#xff1f;PoE Overlay正是为你量身打造的解决方案&#xff0c;这款基于Overwolf平台的游戏辅助工具将彻底改变你的游戏体验。…

作者头像 李华
网站建设 2026/2/27 3:25:41

黑极光君与面包君的对话15

面包君&#xff1a;真正的超越&#xff0c;不是在竞争和比较中胜出&#xff0c;而是高出竞争和比较的维度&#xff0c;让这些竞争和比较直接失去意义&#xff0c;就像是旧系统是一栋10层普通平民楼房&#xff0c;我盖出一栋100层更加宽敞且结构稳固的摩天大楼&#xff0c;还欢迎…

作者头像 李华
网站建设 2026/3/13 0:44:56

AMD显卡AI图像生成:突破兼容性壁垒的终极性能优化方案

AMD显卡AI图像生成&#xff1a;突破兼容性壁垒的终极性能优化方案 【免费下载链接】ComfyUI-Zluda The most powerful and modular stable diffusion GUI, api and backend with a graph/nodes interface. Now ZLUDA enhanced for better AMD GPU performance. 项目地址: htt…

作者头像 李华
网站建设 2026/3/14 1:16:57

Jupyter AI终极指南:从零开始快速上手AI编程助手

Jupyter AI终极指南&#xff1a;从零开始快速上手AI编程助手 【免费下载链接】jupyter-ai A generative AI extension for JupyterLab 项目地址: https://gitcode.com/gh_mirrors/ju/jupyter-ai Jupyter AI是一个专为JupyterLab设计的生成式AI扩展&#xff0c;它巧妙地将…

作者头像 李华
网站建设 2026/3/11 20:48:20

java springboot基于微信小程序的企业问卷调查投票系统(源码+文档+运行视频+讲解视频)

文章目录 系列文章目录目的前言一、详细视频演示二、项目部分实现截图三、技术栈 后端框架springboot前端框架vue持久层框架MyBaitsPlus微信小程序介绍系统测试 四、代码参考 源码获取 目的 摘要&#xff1a;传统企业问卷调查与投票方式存在效率低、数据收集难、参与度不高等…

作者头像 李华