嘿,各位正在 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。
TodoInput调用父组件传来的方法,把新数据传回父组件(子传父)。- 父组件更新
todos状态。 - 父组件把更新后的
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.jsx 的 addTodo 方法中,是这样处理的:
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 的三大纪律
- 稳定性 :ID 生成后就不应该变(所以不能用
Math.random(),因为它每次渲染都会变)。 - 唯一性:在当前列表中,不能有两个相同的 ID。
- 预测性 :通过 ID 我们可以快速在
setTodos中定位数据,比如todos.filter(t => t.id !== id)。
九、 数据流操作:添加、删除与切换
在 App.jsx 中,我们定义了几个关键操作:
- 添加 (addTodo) : 使用解构赋值
[...todos, newTodo]保证数据的不可变性 (Immutability)。不要用push!
arduino
const addTodo = (text) => {
setTodos([...todos, {
id: Date.now(),// 时间戳
text,
completed: false,
}]);
}
-
删除 (deleteTodo) : 使用
filter。JavaScript
iniconst deleteTodo = (id) => { setTodos(todos.filter(todo => todo.id !== id)); } -
切换状态 (toggleTodo) : 使用
map。JavaScript
iniconst toggleTodo = (id) => { setTodos(todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo )); }💡 专业术语: 这里体现了 "数据驱动视图" 。子组件只需发出一个"请求"(调用 ID),由父组件统一更新数据,正确且高效。
十、 总结:React 通信全景图
通过这个项目,我们要记住 React 组件通信的三板斧:
- 父传子: 通过
props直接传。 - 子传父: 父传一个 callback 函数给子,子在需要时调用。
- 兄弟传: 状态提升到父组件,通过父组件当中转站。
为什么子组件不能直接修改数据?
因为 "统一,正确" 。如果每个子组件都能随意修改父组件的数据,调试代码时你会发现根本找不着是谁把数据改坏了。单向数据流保证了数据的可追溯性。
希望这篇文章能帮你搞定 React 组件通信!如果觉得有用,记得点赞、收藏、关注三连哦!我们下期再见!🚀