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

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax