🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信
本文通过一个真实的 TodoList 项目,手把手带你理解 React 父子组件通信的核心模式:状态提升 + Props 下传 + 事件上达。附完整可运行代码 + TypeScript 类型定义 + React DevTools 调试技巧,建议收藏反复阅读。
📖 前言
你刚写完一个 TodoList,在子组件里删了一条任务------子组件里消失了,但父组件纹丝不动,刷新一下数据又回来了。
写出来不报错,但数据就是不同步;改了子组件,父组件没反应......你以为是 bug,其实是你没搞懂父子组件通信的模式。
90% 的 React 新手都卡在这里。它是 React 单向数据流的核心体现,也是你写任何 React 应用都绕不开的基础。
今天,我们用一个 TodoList 项目,从原理到代码到调试,一次性讲透。
💡 读完你将获得:
- ✅ React 父子组件通信的完整模式(3 个组件 × 4 种通信方式)
- ✅ 3 个新手高频踩坑点 + 避坑方法
- ✅ React DevTools 调试数据流的实战技巧
- ✅ 一套可直接
npm run dev跑起来的完整代码
🚀 快速开始
bash
# 克隆项目
git clone https://gitee.com/zhou-wenqiang-c/ai_doubao_zwq.git
# 进入项目目录
cd ai_doubao_zwq/fe/react/basic/todos
# 安装依赖
npm install
# 启动开发服务器
npm run dev
💡 环境要求 :Node.js >= 18.0.0,npm >= 9.0.0。启动后浏览器打开
http://localhost:5173即可看到效果。
📁 完整源码 :Gitee 仓库地址
启动后,你会看到如下界面:
text
┌─────────────────────────────────────┐
│ My Todo List │
│ │
│ ┌───────────────────────┐ ┌─────┐ │
│ │ What needs to be done?│ │ Add │ │
│ └───────────────────────┘ └─────┘ │
│ │
│ ☑ 吃饭 │
│ ☑ 睡觉 │
│ ☐ 打豆豆 (有删除线) │
│ │
│ Total: 3 | Active: 2 | Done: 1 │
│ [Clear Completed] │
└─────────────────────────────────────┘
🎯 小挑战:在看下面的代码之前,先想想------如果让你来拆分组件,你会怎么拆?哪些数据应该放在哪个组件里?
🏗️ 项目结构
text
todos/
├── src/
│ ├── main.jsx # 入口文件
│ ├── App.jsx # 父组件(状态管理中心)
│ ├── App.css # 组件样式(详见 Gitee 仓库)
│ └── components/
│ ├── TodoInput.jsx # 子组件:输入框
│ ├── TodoList.jsx # 子组件:列表展示
│ └── TodoStates.jsx # 子组件:状态统计
├── package.json
└── vite.config.js
入口文件很简单,就是把 App 挂载到 DOM:
jsx
// main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
💡
StrictMode会让组件在开发环境下渲染两次,方便你发现副作用问题,生产环境不会。
三个子组件各司其职:
| 组件 | 职责 | 通信方向 |
|---|---|---|
TodoInput |
输入新任务 | 子 → 父(上报新任务) |
TodoList |
展示列表、勾选、删除 | 父 → 子(传数据)+ 子 → 父(上报操作) |
TodoStates |
统计数量、清除已完成 | 父 → 子(传统计)+ 子 → 父(上报清除) |
🔑 核心原则:状态提升(Lifting State Up)
React 的数据流是 单向的、自上而下的。当多个组件需要共享同一份数据时,React 的解决方案是:
把状态"提升"到它们共同的父组件中,由父组件统一管理。
在我们的项目中,todos 数组就是被提升到 App 组件中的共享状态:
jsx
// App.jsx --- 父组件是唯一的状态持有者
import { useState } from 'react'
import TodoInput from './components/TodoInput'
import TodoList from './components/TodoList'
import TodoStates from './components/TodoStates'
const App = () => {
const [todos, setTodos] = useState([
{ id: 1, text: '吃饭', completed: false },
{ id: 2, text: '睡觉', completed: false },
{ id: 3, text: '打豆豆', completed: true }
])
// 派生状态:从 todos 计算得出,不需要单独的 useState
const activeCount = todos.filter(todo => !todo.completed).length
const completedCount = todos.length - activeCount
// 4 个修改状态的函数(后面会逐一讲解通信原理)
const addTodo = (text) => {
if (text.trim() === '') return
setTodos([{ id: Date.now(), text, completed: false }, ...todos])
}
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed))
}
return (
<div>
<h1>My Todo List</h1>
<TodoInput onAdd={addTodo} />
<TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
<TodoStates
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
</div>
)
}
为什么不让每个子组件自己管自己的数据? 因为 TodoList 需要展示列表,TodoStates 需要统计数量------它们依赖同一份 todos。如果各自维护一份,数据就会不同步。
💬 想想看 :如果把
todos放在TodoList组件里,TodoStates怎么拿到总数?你能想到几种方案?评论区聊聊!
流程图片

📤 父 → 子:通过 Props 传递数据
父组件通过 props 把数据和回调函数传给子组件。这是最基础的通信方式。
1. 传递数据
jsx
// App.jsx
<TodoList
todos={todos} // 传递数据
onToggle={toggleTodo} // 传递回调
onDelete={deleteTodo} // 传递回调
/>
子组件通过解构 props 接收:
jsx
// TodoList.jsx
const TodoList = ({ todos, onToggle, onDelete }) => {
return (
<ul className="todo-list">
{todos.length === 0 ? (
<li className="empty">No todos yet!</li>
) : (
todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
</label>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
))
)}
</ul>
)
}
2. 传递统计信息
TodoStates 只需要展示数据,不需要修改:
jsx
// App.jsx
<TodoStates
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
jsx
// TodoStates.jsx
const TodoStates = ({ total, active, completed, onClearCompleted }) => {
return (
<div className="todo-states">
<p>Total: {total} | Active: {active} | Completed: {completed}</p>
{completed > 0 && (
<button onClick={onClearCompleted} className="clear-btn">
Clear Completed
</button>
)}
</div>
)
}
总结:父 → 子的通信就是把数据"塞"进 props 里,子组件直接用就行。
🎯 不看代码试试 :你能不看上面的代码,自己写出
TodoStates组件吗?试试看,再回来对照!
📥 子 → 父:通过回调函数上报事件
这是初学者最容易困惑的部分。核心思路是:
父组件把一个函数通过 props 传给子组件,子组件在合适的时机调用这个函数,把数据"带回去"。
⚠️ 先分清两种事件
🎯 先猜猜看:子组件怎么把数据"带回去"给父组件?是像快递一样自动送达,还是像打电话一样需要主动拨号?想好了再往下看!
在看代码之前,必须先搞清楚一个关键区别------原生 DOM 事件 和自定义事件:
| 类型 | 事件 | 来源 | 示例 |
|---|---|---|---|
| 原生 DOM 事件 | onSubmit, onChange, onClick |
浏览器内置 | <form onSubmit={...}> |
| 自定义事件 | onAdd, onToggle, onDelete |
开发者自己定义 | <TodoInput onAdd={addTodo}> |
🔑 关键区别:原生事件是浏览器自动触发的(用户点了按钮、表单提交了......);自定义事件是开发者命名的,本质是一个通过 props 传下来的回调函数,需要子组件主动调用才会触发。
1. 添加任务:TodoInput → App
这是子 → 父通信最典型的例子,我们逐行拆解:
父组件 App.jsx:定义回调,通过 props 传下去
jsx
// App.jsx
const addTodo = (text) => {
if (text.trim() === '') return
// Date.now() 返回数字时间戳作为 ID,教学场景够用
// ⚠️ 生产环境建议用 nanoid 或 uuid 库,避免极端情况下的 ID 重复
setTodos([{ id: Date.now(), text, completed: false }, ...todos])
}
<TodoInput onAdd={addTodo} />
子组件 TodoInput.jsx:接收回调,在合适的时机调用
jsx
// TodoInput.jsx
import { useState } from 'react'
const TodoInput = ({ onAdd }) => {
// ① 局部状态:只有输入框自己需要
const [inputValue, setInputValue] = useState('')
// ③ 提交时:调用父组件的回调 + 清空输入框
const handleSubmit = (e) => {
e.preventDefault() // 阻止表单默认刷新页面(这是原生事件!)
onAdd(inputValue) // 调用父组件传来的回调(这是自定义事件!)
setInputValue('') // 清空输入框
}
return (
// ② onSubmit 是原生 DOM 事件,浏览器在表单提交时自动触发
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
// onChange 也是原生 DOM 事件,每次敲字都触发
onChange={(e) => setInputValue(e.target.value)}
placeholder="What needs to be done?"
autoFocus
/>
<button type="submit">Add</button>
</form>
)
}
完整数据流拆解:
text
用户敲字 "吃饭"
↓
原生事件 onChange 触发 → setInputValue("吃饭") → inputValue = "吃饭"
↓
输入框显示 "吃饭"(因为 value={inputValue},这就是「受控组件」模式------输入框的值由 React 状态控制)
↓
用户按回车 / 点 Add
↓
原生事件 onSubmit 触发 → handleSubmit() 执行
↓
├── e.preventDefault() ← 阻止表单刷新页面
├── onAdd("吃饭") ← 调用父组件传来的回调(自定义事件)
│ ↓
│ 父组件 App 的 addTodo("吃饭") 执行
│ ↓
│ setTodos([{id:..., text:"吃饭"}, ...todos])
│ ↓
│ 界面更新,新任务出现在列表里
│
└── setInputValue('') ← 输入框清空,等待下一次输入
关键点:
onSubmit和onChange是原生 DOM 事件,浏览器自动触发onAdd是自定义事件,本质是父组件通过 props 传下来的回调函数- 子组件不需要知道"添加任务"的实现细节,它只负责报告事件
2. 切换完成状态:TodoList → App
jsx
// App.jsx
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
))
}
// TodoList.jsx --- onChange 是原生事件,onToggle 是自定义事件
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)} // ← 原生事件触发后,调用自定义事件
/>
3. 删除任务:TodoList → App
jsx
// App.jsx
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id))
}
// TodoList.jsx --- onClick 是原生事件,onDelete 是自定义事件
<button onClick={() => onDelete(todo.id)}>删除</button>
🎯 不看代码试试 :你能自己写出
clearCompleted的完整回调链吗?从 App 定义函数 → 传给 TodoStates → TodoStates 调用,试试看再回来对照!💡 规律 :子 → 父通信的套路永远是------原生 DOM 事件触发 → 在回调函数里调用父组件传来的自定义事件。
🧠 数据流全景图
把上面的通信关系画成图,整个数据流一目了然:
App(父组件)是中心枢纽,持有所有状态,向下传数据,向上收事件:
text
App (父组件)
state: todos, activeCount, completedCount
┌──────┼──────┐
↓ ↓ ↓
TodoInput TodoList TodoStates
数据向下流(props):
- App → TodoInput:传
onAdd回调 - App → TodoList:传
todos数据 +onToggle+onDelete回调 - App → TodoStates:传
total+active+completed统计 +onClearCompleted回调
事件向上流(callback):
- TodoInput → App:调用
onAdd(text)上报新任务 - TodoList → App:调用
onToggle(id)上报勾选 /onDelete(id)上报删除 - TodoStates → App:调用
onClearCompleted()上报清除请求
各子组件职责:
| 组件 | 有局部状态? | 能修改数据? | 职责 |
|---|---|---|---|
| TodoInput | ✅ inputValue |
❌ 只上报 | 输入框 UI + 上报新任务 |
| TodoList | ❌ | ❌ 只上报 | 纯展示列表 + 上报勾选/删除 |
| TodoStates | ❌ | ❌ 只上报 | 纯展示统计 + 上报清除 |
两条核心规则:
- 数据自上而下:父组件通过 props 把数据传给子组件
- 事件自下而上:子组件通过调用父组件传来的回调函数上报事件
🎯 动手试试:试着画一下你当前项目的组件通信图,标出哪些是 props 向下、哪些是 callback 向上,你会发现自己对项目的理解更深了。
💡 设计原则:哪些状态该放父组件?
| 状态类型 | 放哪里 | 示例 |
|---|---|---|
| 共享状态(多个组件依赖) | 父组件 | todos 数组 |
| 局部 UI 状态(只有自己用) | 子组件 | inputValue 输入框内容 |
| 派生状态(从共享状态计算) | 父组件 | activeCount、completedCount |
在我们的项目中:
todos→ 放在App,因为TodoList和TodoStates都需要inputValue→ 放在TodoInput,因为只有输入框自己需要activeCount→ 放在App,从todos派生计算
💼 真实业务场景
TodoList 是经典教学案例,但这个模式在实际工作中无处不在:
场景一:省市区三级联动
text
父组件 AddressForm
├── state: { province, city, district }
├── <ProvinceSelect onChange={setProvince} /> ← 选省
├── <CitySelect province={province} onChange={setCity} /> ← 选市
└── <DistrictSelect city={city} onChange={setDistrict} /> ← 选区
选了省之后,市的列表要更新;选了市之后,区的列表要更新。province 和 city 必须放在父组件,因为子组件之间有依赖关系------这和 todos 放在 App 是同一个道理。
场景二:多步骤表单(向导)
text
父组件 Wizard
├── state: { step, formData }
├── <Step1 data={formData} onNext={saveStep1} />
├── <Step2 data={formData} onNext={saveStep2} onBack={goBack} />
└── <Step3 data={formData} onSubmit={handleSubmit} />
当前步骤 step 和表单数据 formData 必须在父组件管理,因为步骤之间需要共享数据,最后还要统一提交。
💡 规律:只要多个子组件之间有数据依赖或需要协作,就用「状态提升 + 回调上报」的模式。
⚠️ 常见错误分析
下面这 3 个坑,你踩过几个?先自己想想再看答案 👇
1. 子组件直接修改 props
jsx
// ❌ 错误:子组件直接修改父组件传来的数据
const TodoList = ({ todos }) => {
todos.push({ id: 4, text: '新任务' }) // 不要这样做!
// 运行后:界面可能不会更新(React 检测不到引用变化)
// 或者:其他组件读到的还是旧数据,因为引用没变
}
// ✅ 正确:通过回调通知父组件修改
const TodoList = ({ todos, onAdd }) => {
// 让父组件来修改状态
}
React 的 props 是只读的,子组件永远不应该直接修改 props。原因有两个:
- 违反单向数据流原则------父组件不知道数据被改了,其他依赖同一数据的组件也不会更新
- 可能影响共享同一引用的其他组件------JavaScript 中对象是引用传递,改了一个地方,所有引用的地方都会变
💬 你遇到过这个坑吗?很多从 Vue 转过来的同学会习惯性地直接修改 props,评论区说说你的经历!
2. 子组件自己处理了逻辑,忘了通知父组件
jsx
// ❌ 容易出错:子组件自己过滤了列表,但父组件的 todos 没变!
const TodoList = ({ todos, onDelete }) => {
const handleDelete = (id) => {
const filtered = todos.filter(todo => todo.id !== id)
console.log('删除后:', filtered)
// 刷新后数据又回来了,因为父组件的状态从未被更新
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>删除</button>
</li>
))}
</ul>
)
}
// ✅ 正确:调用父组件传来的回调,让父组件更新状态
const TodoList = ({ todos, onDelete }) => {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
))}
</ul>
)
}
🐛 这个 bug 很隐蔽:界面上看起来删掉了,但刷新页面又出现了------因为子组件根本没通知父组件去更新状态。
💬 你有没有遇到过"操作成功但刷新就恢复"的 bug?大概率就是这个原因,评论区聊聊!
3. 混淆原生事件和自定义事件
jsx
// ❌ 错误写法:以为 onAdd 会像 onSubmit 一样自动触发
const TodoInput = ({ onAdd }) => {
const [inputValue, setInputValue] = useState('')
return (
<form onSubmit={(e) => e.preventDefault()}>
<input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<button type="submit">Add</button>
{/* 以为点 Add 就会自动调用 onAdd?不会! */}
{/* 运行结果:点按钮后什么都没发生,任务没有被添加 */}
</form>
)
}
// ✅ 正确写法:在 onSubmit 回调里手动调用 onAdd
const TodoInput = ({ onAdd }) => {
const [inputValue, setInputValue] = useState('')
const handleSubmit = (e) => {
e.preventDefault() // 原生事件:阻止表单刷新
onAdd(inputValue) // 自定义事件:必须手动调用!
setInputValue('')
}
return (
<form onSubmit={handleSubmit}>
<input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<button type="submit">Add</button>
</form>
)
}
🐛 运行效果对比 :错误写法中,点 Add 按钮后表单提交了(触发了原生 onSubmit),但
onAdd从未被调用,所以任务不会被添加------因为自定义事件不会自动触发,必须在代码里显式调用。💬 类比理解:原生事件就像快递自动送到你家(浏览器自动触发);自定义事件就像客服给你的电话号码(props 传下来的回调),你得自己主动拨号(调用函数)才能联系上。
🔄 单向数据流的优势
你可能会问:为什么要这么麻烦?直接让子组件修改全局状态不行吗?
React 坚持单向数据流是有原因的:
- 可预测性:数据从哪来、谁改的,一目了然
- 易调试:状态只在父组件修改,出问题直接定位
- 易维护:每个组件的职责清晰,不会出现"不知道谁改了数据"的混乱
- 易测试:子组件是纯展示组件,测试简单
text
用户点击删除按钮
↓
TodoList 调用 onDelete(id) ← 自定义事件(子组件主动调用)
↓
App 执行 deleteTodo(id)
↓
App 调用 setTodos() 更新状态
↓
React 重新渲染 App 及其子组件
↓
TodoList 收到新的 todos props,界面更新
整个流程清晰可控,没有"暗箱操作"。
🎯 动手试试 :打开 React DevTools,找到
TodoList组件,试着在 Console 里执行$r.props.todos.push({id:99, text:'测试'}),然后观察TodoStates的数字有没有变化------你会发现没变化,因为 React 检测不到引用变化。这就是为什么必须通过setTodos更新。
🔧 用 React DevTools 验证数据流
理论讲完了,怎么验证?装一个 React Developer Tools 浏览器扩展(Chrome 插件),你就能直观看到:
-
查看 Props 传递 :在 DevTools 的 Components 面板中,点击
TodoList组件,右侧面板会显示它收到的所有 props:todos(数组)、onToggle(函数)、onDelete(函数) -
追踪状态变化 :点击
App组件,可以看到todos的当前值。当你勾选一个任务时,观察todos数组中对应对象的completed字段变化 -
验证重新渲染 :DevTools 的 Profiler 面板可以录制渲染过程。勾选一个 todo 后,你会看到
App→TodoList→ 每个TodoItem依次重新渲染
text
React DevTools → Components 面板
▶ <App>
todos: [{id: 1, text: "吃饭", completed: false}, ...]
▼ <TodoInput>
onAdd: ƒ addTodo() ← 自定义事件(父组件传来的回调)
inputValue: "" ← 子组件自己的状态(DevTools 里看不到)
▼ <TodoList>
todos: [{...}, {...}, ...] ← 父组件传来的数据
onToggle: ƒ toggleTodo() ← 自定义事件
onDelete: ƒ deleteTodo() ← 自定义事件
▼ <TodoStates>
total: 3
active: 2
completed: 1
onClearCompleted: ƒ clearCompleted()
💡 调试技巧:如果某个组件的 props 没有按预期更新,先检查父组件是否正确传递了回调;如果子组件的修改没有反映到界面上,大概率是忘了调用 props 中的回调函数。
🎯 动手验证:打开你自己的项目,用 DevTools 找到一个组件,看看它收到了哪些 props?哪些是数据、哪些是回调函数?评论区分享你的发现!
🎯 总结
通过这个 TodoList 项目,我们掌握了 React 父子组件通信的核心模式:
| 通信方向 | 实现方式 | 示例 |
|---|---|---|
| 父 → 子 | 通过 props 传递数据 | todos={todos} |
| 子 → 父 | 通过回调函数上报事件 | onAdd={addTodo} |
| 共享状态 | 提升到共同父组件 | const [todos, setTodos] = useState(...) |
| 局部状态 | 留在子组件自己管理 | const [inputValue, setInputValue] = useState(...) |
记住这四句话,你就掌握了 React 组件通信的核心:
- 状态在哪里,修改它的方法就在哪里
- 数据通过 props 自上而下流动
- 事件通过回调函数自下而上报告
- 子组件永远不要直接修改 props
最后,再强调一下两种事件的区别:
| 原生 DOM 事件 | 自定义事件 | |
|---|---|---|
| 来源 | 浏览器内置 | 开发者自定义 |
| 触发 | 自动(用户操作) | 手动(代码调用) |
| 例子 | onSubmit, onChange, onClick |
onAdd, onToggle, onDelete |
| 本质 | DOM 事件对象 | 通过 props 传下来的回调函数 |
💎 TypeScript 加分项
如果你用 TypeScript,父子组件通信的类型定义会更清晰:
tsx
// 定义 Todo 的类型
interface Todo {
id: number
text: string
completed: boolean
}
// 子组件 Props 类型 --- 明确每个回调的参数和返回值
interface TodoListProps {
todos: Todo[]
onToggle: (id: number) => void // 参数是 number,无返回值
onDelete: (id: number) => void
}
const TodoList = ({ todos, onToggle, onDelete }: TodoListProps) => {
// ...TS 会在编译时检查你是否正确传递了 props
}
// TodoInput 的自定义事件类型
interface TodoInputProps {
onAdd: (text: string) => void // 参数是 string,无返回值
}
const TodoInput = ({ onAdd }: TodoInputProps) => {
// ...
}
💡 TS 的好处 :如果你漏传了
onDelete,或者传了一个参数类型不对的函数,编辑器会立刻报错------不用等到运行时才发现问题。🎯 动手试试 :把
TodoInput.jsx改名为TodoInput.tsx,然后给onAdd加上类型(text: string) => void,看看编辑器会不会自动提示参数类型------体验一下 TS 的好处!
🚀 进阶思考
掌握了基础的父子通信后,你可以继续探索:
- Context API:当组件层级很深时,避免 props 逐层传递("prop drilling")
- useReducer:当状态逻辑变复杂时,用 reducer 替代多个 useState
- useCallback :当回调函数作为 props 传给子组件时,用
useCallback包裹可以避免不必要的重新渲染 - 状态管理库(Zustand、Jotai):当应用规模变大时,全局状态管理
但不管用什么方案,父子组件通信的模式都是基础中的基础。理解了它,后面的一切都是在这个基础上做封装和优化。
💬 互动时间
思考题 :如果要给这个 TodoList 加上「编辑」功能(双击任务可以修改文字),你觉得回调函数该怎么设计?
onEdit(id, newText)还是其他方式?欢迎在评论区分享你的思路!
小挑战 :试着给这个项目加上「全选/取消全选」功能。提示:你需要一个新的回调函数onToggleAll,思考一下它应该放在哪个组件里?
📚 下一篇预告 :我们将用useReducer重构这个 TodoList,看看当状态逻辑变复杂时,如何用 reducer 模式让代码更清晰。感兴趣的话记得关注 + 收藏,不迷路!
💖 如果这篇文章对你有帮助,请点个赞让更多人看到!有任何问题欢迎在评论区讨论,我会一一回复。
关注我 @前端学习笔记,持续分享 React 实战干货 📚