彻底搞懂 React 组件通信:从 TodoList 实战出发,解锁 React 开发的“核心姿势” 🚀

嘿,各位正在 React 门前反复横跳的新手小伙伴们!👋

是不是经常被"数据该放哪"、"怎么传给子组件"、"子组件想改父组件数据怎么办"这三个终极哲学问题搞得头大?别担心,今天咱们不聊虚的,直接通过一个经典的 React + Stylus + Vite 实战项目------Todos,带你一次性打通 React 组件通信的任督二脉!

不仅有代码,还有深度解析。准备好咖啡,我们要开始"套娃"了!


一、 项目背景:为什么我们要"套娃"?

在 Vue 里,你可能习惯了 v-model 的便捷,但在 React 的世界里,一切都是单向数据流。数据就像顺流而下的河水,从父组件流向子组件。

我们的项目结构如下:

  • App.js (大管家):持有所有数据(todos),负责逻辑处理。
  • TodoInput (输入框):负责产生新任务。
  • TodoList (展示列表):展示任务,并允许用户勾选完成或删除。
  • TodoStats (统计看板):展示剩余任务,提供一键清理。

二、 环境准备:Stylus 与 Vite 的碰撞

首先,我们使用的是 Vite 环境。在 React 中引入 CSS 预处理器(如 Stylus)非常简单。

1. 如何引入 Stylus

在 Vite 中,你只需要安装 stylus

csharp 复制代码
npm init stylus

然后像这样在 App.jsx 中引入即可:

JavaScript

arduino 复制代码
import './styles/app.styl' // 直接引入,Vite 会自动帮你处理编译

为什么用 Stylus? 因为它简洁,没有大括号和分号的束缚,和 React 的组件化思维很搭。


三、 核心灵魂:App 组件(数据中心化)

在 React 中,如果多个组件(比如输入框和列表)需要共享同一份数据,最正宗的做法就是状态提升(Lifting State Up) 。我们将 todos 放在它们的共同父组件 App 中。

1. useState 的高级用法:惰性初始化

看这行代码:

JavaScript

ini 复制代码
const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos');
  return saved ? JSON.parse(saved) : [];
});

💡 超级关键点: useState 可以接收一个函数作为参数。这叫"惰性初始化"。

为什么要这么做? 如果直接写 localStorage.getItem,每次组件重新渲染(render)时都会执行一遍 IO 读取。传一个函数,React 只会在组件第一次挂载时执行它。性能优化,从细节做起!

2. useEffect 的副作用管理

我们要实现"持久化存储",即刷新页面数据不丢。

JavaScript

javascript 复制代码
useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]); // 只有当 todos 发生变化时,才会触发保存

这里使用了 useEffect。它的第二个参数 [todos] 是依赖项,保证了我们只在数据变动时才去写磁盘,优雅!


四、 兄弟组件通信:间接的"曲线救国"

很多新手问:TodoInput 产生的数据,怎么传给 TodoList?

答案: 兄弟组件之间不能直接打招呼!它们必须通过共同的"老爹" App。

  1. TodoInput 调用父组件传来的方法,把新数据传回父组件(子传父)。
  2. 父组件更新 todos 状态。
  3. 父组件把更新后的 todos 传给 TodoList(父传子)。

这就是 "父组件负责持有数据,管理数据" 的核心原则。


五、 子父通信:自定义事件的"上报"

由于 React 的 props 是只读的,子组件绝对不能直接修改父组件传过来的变量。

1. 子组件如何修改父组件的自由变量?

秘诀: 父组件不仅把数据传给子,还把"修改数据的方法"也传过去。

JavaScript

javascript 复制代码
// App.jsx 中
const addTodo = (text) => {
  setTodos([...todos, {
    id: Date.now(), // 使用时间戳作为唯一 ID
    text,
    completed: false,
  }]);
}

return (
  <TodoInput onAdd={addTodo} /> // 传递方法
)

💡 超级关键点:唯一 ID。 遍历数据(map)时必须有 key。为什么?React 用虚拟 DOM 算法比对差异时,靠 key 识别哪个元素变了。如果用 index,删掉中间一个元素会导致后续所有元素重绘,性能炸裂。这里我们用 Date.now() 快速生成唯一 ID。


六、 详解 TodoInput:模拟"双向绑定"

React 不支持 v-model,因为它推崇"显式优于隐式"。我们要实现类似功能,需要通过 单向绑定 + onChange 监听

JavaScript

ini 复制代码
const TodoInput = ({ onAdd }) => {
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交刷新页面
    if(!inputValue.trim()) return; 
    onAdd(inputValue); // 调用父组件传来的函数
    setInputValue(''); // 清空输入框
  }

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={inputValue} // 绑定状态
        onChange={e => setInputValue(e.target.value)} // 监听输入
      />
      <button type="submit">Add</button>
    </form>
  )
}

逻辑闭环: 状态改变 -> 触发 onChange -> 更新 inputValue -> 视图重新渲染。虽然麻烦一点,但每一步都清清楚楚!


七、 详解 TodoList:Props 的清晰解构

在子组件中处理 props 时,推荐直接在函数参数里或者函数体第一行进行解构。

JavaScript

javascript 复制代码
const TodoList = (props) => {
  const { 
      todos,
      onDelete,
      onToggle 
      } = props; // 清晰的解构
  // ... 后面直接使用 todos,而不是 props.todos
}

这样做的好处是:一眼就能看出这个组件依赖哪些数据,代码阅读感拉满。

列表渲染与三目运算符

TodoList 中,我们使用了大量的三目运算符来控制视图:

JavaScript

ini 复制代码
{todos.length === 0 ? (
  <li className="empty">No todos yet!</li>
) : (
  todos.map(...)
)}

这是 React 的基本功。记住:React 的大括号 {} 里可以写任何 JS 表达式。三目运算符是实现条件渲染最干净的方式。


八、为什么 ID 必须是"唯一"的?

TodoList 组件里,我们看到 todos.map 循环时,每个 <li> 都有一个 key={todo.id}。很多新手为了省事会直接用数组的索引 index,但这正是万恶之源

1. 为什么不能用 Index?

React 在更新 DOM 时,会通过 key 来判断哪些元素是新加的、哪些被删除了。

  • 情景模拟:如果你有三个任务 A、B、C,索引分别是 0、1、2。当你删掉了中间的 B,剩下的 A 和 C 索引就变成了 0 和 1。
  • React 的困惑:React 会以为你删掉了 C(原来的索引 2 没了),然后把 B 的内容改成了 C。这不仅浪费性能,在涉及表单输入或动画时,还会产生非常诡异的 UI Bug。

2. 代码中如何实现"唯一 ID"?

在我们的 App.jsxaddTodo 方法中,是这样处理的:

JavaScript

arduino 复制代码
const addTodo = (text) => {
  setTodos([...todos, {
    // 💡 超级关键点:使用时间戳生成唯一 ID
    id: Date.now(), 
    text,
    completed: false,
  }]);
}

专业讲解:

  • Date.now() :它返回自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的毫秒数。对于像 TodoList 这种个人使用的单机应用,用户点击按钮的速度是不可能超过 1 毫秒一次的,所以这个数字在当前应用中是绝对唯一的。
  • 更专业的方案 :在大型商业项目中,我们通常会使用 crypto.randomUUID() 或者 uuid 库来生成更长、更复杂、碰撞率几乎为零的字符串 ID。

3. 渲染时的"身份标识"

TodoList.jsx 中:

JavaScript

ini 复制代码
{todos.map(todo => (
  <li key={todo.id} className={todo.completed ? 'completed' : ''}>
    {/* ...内容 */}
  </li>
))}

有了这个 todo.id,React 的 Diff 算法 (找差异的算法)就能像激光手术一样精准:它知道你只是删掉了 ID 为 1734950400000 的那一项,而其他项完全不需要重新渲染。


4. ID 的三大纪律

  1. 稳定性 :ID 生成后就不应该变(所以不能用 Math.random(),因为它每次渲染都会变)。
  2. 唯一性:在当前列表中,不能有两个相同的 ID。
  3. 预测性 :通过 ID 我们可以快速在 setTodos 中定位数据,比如 todos.filter(t => t.id !== id)

九、 数据流操作:添加、删除与切换

App.jsx 中,我们定义了几个关键操作:

  1. 添加 (addTodo) : 使用解构赋值 [...todos, newTodo] 保证数据的不可变性 (Immutability)。不要用 push
arduino 复制代码
const addTodo = (text) => {
    setTodos([...todos, {
      id: Date.now(),// 时间戳
      text,
      completed: false,
    }]);
  }
  1. 删除 (deleteTodo) : 使用 filter

    JavaScript

    ini 复制代码
    const deleteTodo = (id) => {
      setTodos(todos.filter(todo => todo.id !== id));
    }
  2. 切换状态 (toggleTodo) : 使用 map

    JavaScript

    ini 复制代码
    const toggleTodo = (id) => {
      setTodos(todos.map(todo => 
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ));
    }

    💡 专业术语: 这里体现了 "数据驱动视图" 。子组件只需发出一个"请求"(调用 ID),由父组件统一更新数据,正确且高效。


十、 总结:React 通信全景图

通过这个项目,我们要记住 React 组件通信的三板斧:

  1. 父传子: 通过 props 直接传。
  2. 子传父: 父传一个 callback 函数给子,子在需要时调用。
  3. 兄弟传: 状态提升到父组件,通过父组件当中转站。

为什么子组件不能直接修改数据?

因为 "统一,正确" 。如果每个子组件都能随意修改父组件的数据,调试代码时你会发现根本找不着是谁把数据改坏了。单向数据流保证了数据的可追溯性。


希望这篇文章能帮你搞定 React 组件通信!如果觉得有用,记得点赞、收藏、关注三连哦!我们下期再见!🚀

相关推荐
leafff1232 小时前
一文了解:Multi-Agent多智能体核心架构的协调与调度模块是如何工作的?
架构
若梦plus2 小时前
实现WebView JSBridge
前端
JS_GGbond2 小时前
揭秘微信扫码登录:那个小绿框背后的魔法
前端
C_心欲无痕2 小时前
vue3 - 响应式数ref与reactive的深度解析
前端·javascript·vue.js
全栈老石2 小时前
TypeScript 中 Type 和 Interface 傻傻分不清?看完这篇就不纠结了
前端·typescript
沈千秋.2 小时前
xss.pwnfunction.com闯关(1~6)
java·前端·xss
浪浪山_大橙子2 小时前
吃透 CSS 常用函数:从布局到美化,18 个高频函数让样式写得又快又优雅
前端