🚀 删了数据刷新又回来?3 组件 × 4 回调 × 3 坑讲透 React 父子通信

🚀 删了数据刷新又回来?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('')    ← 输入框清空,等待下一次输入

关键点:

  • onSubmitonChange原生 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 ❌ 只上报 纯展示统计 + 上报清除

两条核心规则:

  1. 数据自上而下:父组件通过 props 把数据传给子组件
  2. 事件自下而上:子组件通过调用父组件传来的回调函数上报事件

🎯 动手试试:试着画一下你当前项目的组件通信图,标出哪些是 props 向下、哪些是 callback 向上,你会发现自己对项目的理解更深了。


💡 设计原则:哪些状态该放父组件?

状态类型 放哪里 示例
共享状态(多个组件依赖) 父组件 todos 数组
局部 UI 状态(只有自己用) 子组件 inputValue 输入框内容
派生状态(从共享状态计算) 父组件 activeCountcompletedCount

在我们的项目中:

  • todos → 放在 App,因为 TodoListTodoStates 都需要
  • 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} />  ← 选区

选了省之后,市的列表要更新;选了市之后,区的列表要更新。provincecity 必须放在父组件,因为子组件之间有依赖关系------这和 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。原因有两个:

  1. 违反单向数据流原则------父组件不知道数据被改了,其他依赖同一数据的组件也不会更新
  2. 可能影响共享同一引用的其他组件------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 坚持单向数据流是有原因的:

  1. 可预测性:数据从哪来、谁改的,一目了然
  2. 易调试:状态只在父组件修改,出问题直接定位
  3. 易维护:每个组件的职责清晰,不会出现"不知道谁改了数据"的混乱
  4. 易测试:子组件是纯展示组件,测试简单
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 插件),你就能直观看到:

  1. 查看 Props 传递 :在 DevTools 的 Components 面板中,点击 TodoList 组件,右侧面板会显示它收到的所有 props:todos(数组)、onToggle(函数)、onDelete(函数)

  2. 追踪状态变化 :点击 App 组件,可以看到 todos 的当前值。当你勾选一个任务时,观察 todos 数组中对应对象的 completed 字段变化

  3. 验证重新渲染 :DevTools 的 Profiler 面板可以录制渲染过程。勾选一个 todo 后,你会看到 AppTodoList → 每个 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 组件通信的核心:

  1. 状态在哪里,修改它的方法就在哪里
  2. 数据通过 props 自上而下流动
  3. 事件通过回调函数自下而上报告
  4. 子组件永远不要直接修改 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 实战干货 📚

相关推荐
耳东小鹿2 小时前
对象常用方法
前端
程序员海军2 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
前端·后端·程序员
moMo2 小时前
状态管理第一步:React useState 核心行为完全拆解
react.js
程序员黑豆3 小时前
鸿蒙应用开发:Stack堆叠组件实战——实现微信消息角标效果
前端·harmonyos
夏幻灵3 小时前
浏览器原理深度解析:从输入 URL 到页面展现的全流程剖析
前端
会周易的程序员3 小时前
js-shm: 高性能 Node.js 共享内存模块
开发语言·javascript·c++·node.js·共享内存·shm
闲猫3 小时前
Agent工程实践:从WorkFlow到多Agent协作的落地笔记
java·服务器·前端
带娃的IT创业者3 小时前
重新定义前端构建速度:深度解析 SWC 如何用 Rust 颠覆 JavaScript 工具链
前端·javascript·rust·前端构建·swc
happy_0x3f4 小时前
前端应用的离线暂停更新策略
开发语言·前端·php