单向数据流不迷路:用 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 组件通信,直接把这个待办项目甩给他看 ------「喏,都在这儿了~」😎

相关推荐
阿拉伯柠檬2 小时前
MySQL基本查询
linux·数据库·mysql·面试
一水鉴天2 小时前
整体设计 定稿 之 34 codybuddy项目跨机同步方案 之2 (codebuddy)
服务器·前端
朱 欢 庆2 小时前
Jenkins任务执行完成后发送邮件
前端·经验分享·jenkins
前端无涯2 小时前
React/Vue 消息订阅发布:实现方式、开发避坑与面试核心考点
前端·javascript·vue.js
C雨后彩虹2 小时前
幼儿园分班
java·数据结构·算法·华为·面试
一个没有感情的程序猿2 小时前
前端实现交互式3D人体肌肉解剖图:基于 Three.js + React Three Fiber 的完整方案
前端·javascript·3d
武玄天宗2 小时前
第五章、flutter怎么创建底部底部导航栏界面
前端·flutter
Goodbaibaibai2 小时前
接口请求了两次,一次报200,一次报404
前端
qq_463408422 小时前
React Native跨平台技术在开源鸿蒙中使用WebView来加载鸿蒙应用的网页版或通过一个WebView桥接本地代码与鸿蒙应用
javascript·算法·react native·react.js·开源·list·harmonyos