Skip to content

首屏数据渲染与添加todo(Day6 1/3) ​

本页对应 React + TypeScript + 医通项目,共 6 段视频,约 1 小时 3 分。

学习目标 ​

  • 能够使用 useState 声明状态及更新函数,并通过事件触发状态更新完成计数、文本修改等交互。
  • 能够解释 useState 更新函数调用后函数组件重新执行的过程,并说明状态值未改变时函数组件只多渲染一次的现象。
  • 能够使用 useEffect 模拟组件挂载、更新和卸载生命周期,并通过依赖数组控制监听对象。
  • 能够使用 useRef 创建引用对象,获取真实 DOM 元素及用户输入值。
  • 能够综合运用 useState、useEffect 和定时器完成电子时钟组件,并在组件卸载时清理定时器。
  • 能够基于 useState 完成 Todo 列表的首屏数据渲染、添加 Todo 和删除 Todo 功能。

知识点 ​

先明确“首屏数据渲染”要解决什么 ​

TodoList 的首屏渲染,本质是:TodoList 组件先拥有一份 todos 状态数据,首屏渲染时把这份数据通过 props 分发给 Main 和 Footer,Main 负责列表,Footer 负责统计。后续新增待办时,还需要一个不重复的 id 作为列表 key。所以本节有两个前置技术点:生成不重复 id,以及把 todos 定义成 TodoList 的状态。

知识点一:生成不重复的 id ​

为什么需要:列表渲染时 React 需要稳定的 key 来识别每一项。如果 id 重复,列表更新时可能出现复用错乱、勾选状态串行等问题。

怎么想:优先用成熟工具生成唯一值;如果只是练习,也可以利用 Math.random().toString(36) 生成字母数字随机串来模拟。

怎么做:方式一使用 nanoid 包:

bash
npm i nanoid
js
import { nanoid } from 'nanoid'
js
nanoid()

方式二自己模拟生成字母数字随机组合的字符串。笔记中给出:

js
Math.random().toString(36).slice(2)

这里 toString(36) 会把随机数转成 0-9、a-z 组成的 36 进制字符串,slice(2) 去掉前面的 0.。

易错点:nanoid 要按包的实际导入方式使用;自己模拟的随机串长度不固定,也可能碰撞,练习可以,正式项目不要把它当成严格唯一 id。生成 id 的时机应是在新增 todo 时,而不是渲染时临时生成,否则每次渲染 key 都会变化。

知识点二:把 todos 定义成 TodoList 组件的状态 ​

为什么需要:转写稿中反复强调,函数组件要靠 useState 才有状态。若把 todos 写成普通变量,组件重新执行时数据会丢失,也无法触发页面更新。

怎么想:useState 返回一个数组,按位置解构:第一项是状态数据,第二项是修改状态的方法。写法上直接解构,不用像类组件那样考虑 this。

怎么做:在 TodoList 组件中定义:

js
const [todos, setTodos] = useState([
  { id: nanoid(), text: '学习 React', done: false }
])

首屏渲染时,todos 的初始值就是首屏要显示的数据。

易错点:不要直接修改 todos 原数组,例如 todos.push(...);应通过 setTodos 传入新数组。转写稿调试时先打印 useState 的返回值,确认它返回数组,再解构使用,这个习惯可以避免“状态和方法拿反”的问题。

为什么需要:TodoList 是数据持有者,Main 和 Footer 是展示和统计组件。数据放在共同父组件里,子组件通过 props 接收,职责更清晰。

怎么想:React 中父传子用标签属性,子组件通过函数参数接收 props。转写稿中也演示了把状态数据通过属性传给子组件,子组件用参数接收后直接使用。

怎么做:

jsx
<Main todos={todos} />
<Footer todos={todos} />

子组件中:

js
function Main(props) {
  const { todos } = props
  // ...
}

易错点:props 是只读的,不要在 Main 或 Footer 里直接改 props.todos;属性名要保持一致,父组件传 todos,子组件就接收 todos。

知识点四:Main 中 map 渲染列表并把 todo 传给 Item ​

为什么需要:Main 拿到的是数组,需要把每一项转成 JSX。把单个 todo 再传给 Item,可以让 Item 只负责展示一条待办。

怎么想:用 map 遍历 todos,返回 Item 组件;key 使用 todo.id,不要用数组下标。

怎么做:

jsx
function Main({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <Item key={todo.id} todo={todo} />
      ))}
    </ul>
  )
}

易错点:map 必须有返回值;key 要写在 map 返回的最外层组件上。转写稿中“定义 key 值,并将遍历的每一项 todo 传递给 Item”就是这一步的核心。

为什么需要:底部需要显示总数和已完成数,这些数据可以由 todos 派生出来,不必再单独存一份状态,避免数据不同步。

怎么想:total 就是 todos.length;doneNum 是已完成项数量,用 filter 筛出 done 为真的项再取长度。

怎么做:

jsx
function Footer({ todos }) {
  const total = todos.length
  const doneNum = todos.filter(todo => todo.done).length

  return (
    <div>
      总数:{total},已完成:{doneNum}
    </div>
  )
}

易错点:不要用 forEach 后忘记累加;filter 的回调要返回布尔条件。若后续勾选完成状态,应通过 setTodos 生成新数组,Footer 会随着新的 todos 重新计算,而不是手动改 total 和 doneNum。

本节笔记未包含图片,以上保留了笔记中的命令与代码。整体流程可以记成一句话:TodoList 持有 todos 状态 → 通过 props 分发给 Main 和 Footer → Main 用 map 渲染并传 todo 给 Item → Footer 用 todos 计算统计值。

先拆结果,再拆步骤 ​

这一节要做的事很具体:文本框输入内容,按下回车,把内容添加到列表中。
为什么需要先拆?因为直接写代码容易把“取输入、监听键盘、改列表”混在一起,出错后不知道是哪一步坏了。转写稿里反复强调先描述结果,再拆步骤,最后给每一步匹配技术点。

怎么想:先写结果句,再拆成三个动作:

  1. 获取文本框输入内容;
  2. 按下回车;
  3. 将内容添加到列表中。

怎么做:给三步匹配技术点:取输入可用 ref、受控组件、e.target.value;回车可用 onKeyUp / onKeyDown;添加到列表用“子传父”。
易错点:不要一上来就写完整组件,先保证每一步能单独验证。

获取输入:ref、受控组件、e.target.value ​

为什么需要?添加 todo 的第一步是拿到用户输入;拿不到值,后面回车和添加都无从谈起。
怎么想?三条路:ref 直接拿真实 DOM;受控组件把输入同步到 state;事件回调里用 e.target.value 取当前值。
怎么做?转写稿里用 useRef 演示过取 input 值:

jsx
const inputRef = useRef()

<input ref={inputRef} />
<button onClick={() => console.log(inputRef.current.value)}>
  获取
</button>

点击后打印 inputRef.current,看到它是真实 DOM,再取 .value 就是用户输入。受控组件则要同时写 value 和 onChange,让 state 成为输入框的值来源。
易错点:useRef 必须通过 .current 取;受控组件只写 value 不写 onChange 会改不动;e.target.value 要在事件回调里取,不要脱离事件对象乱用。

按下回车:绑定键盘事件 ​

为什么需要?输入框里打字不等于提交,必须明确“回车”这个动作。
怎么想?React 里键盘事件常用 onKeyUp、onKeyDown,事件对象里判断按键。
怎么做?在输入框上绑定事件,例如:

jsx
<input
  onKeyDown={(e) => {
    if (e.key === 'Enter') {
      // 执行添加逻辑
    }
  }}
/>

易错点:事件名大小写要写对,是 onKeyDown / onKeyUp;判断的是 e.key === 'Enter';长按回车可能重复触发,实际项目要按需防重复。

添加到列表:用子传父 ​

为什么需要?输入框和列表常常不在同一个组件里。子组件不能直接改父组件状态,所以要“子传父”。
怎么想?父组件持有列表状态和添加函数;子组件只负责取输入、触发回车,然后调用父组件传下来的函数,把值交上去。
怎么做?父组件把函数作为属性传给子组件;子组件通过 props 接收并调用。转写稿里已有父子传值示例:父组件写 <TestState MSG={MSG} count={count} />,子组件通过参数接收 props.MSG、props.count。添加 todo 也是同一思路:父组件传“添加函数”,子组件在回车时调用它。
易错点:不要直接改 props;列表更新要用新的数组和 setState;子组件调用父函数时要把输入值作为参数传出去。

串起来调试 ​

调试顺序建议:先验证能取到输入值,再验证回车能触发,再验证父组件能收到值,最后看列表是否更新。转写稿里也出现过典型调试:useState 没解构会报 useState is not defined;useRef 绑定后要打印 current 确认拿到真实 DOM;调用 setState 会触发函数组件重新执行并重新渲染。
易错点:不要跳过中间验证;每一步都打印或断点确认,比最后一起猜更可靠。

先明确删除 todo 的数据流 ​

删除 todo 看起来只是“点一下按钮”,但真正要处理的是数据归属问题。转写稿里反复验证过:函数组件用 useState 创建状态,调用 set 方法会触发函数组件重新调用,从而完成页面重新渲染;子组件通过 props 接收父组件数据。删除功能也遵循这条数据流。

为什么需要:todo 列表数据通常放在父组件,删除按钮却在某一行子组件里。子组件不能直接改父组件状态,否则数据流会乱。

怎么想:先找“数据在谁身上”。列表数据在父组件,删除方法就定义在父组件;子组件只负责把被点击行的 id 传出去。

怎么做:父组件定义删除方法,子组件通过 onClick 调用父组件传下来的回调,并把当前行 id 作为参数传回。

易错点:不要以为按钮在子组件,删除逻辑就写在子组件;不要直接修改原数组;不要写成 onClick={onDelete(id)},这样会在渲染时立即执行。

点击删除按钮:onClick 传递 id ​

为什么需要:列表里有很多行,父组件必须知道“删哪一行”。只触发点击事件不够,还要把当前行的唯一标识 id 传出去。

怎么想:删除按钮属于当前 todo 项,当前项身上有 todo.id。点击时用箭头函数包一层,把 id 交给父组件传下来的 onDelete。

怎么做:子组件里写:

jsx
<button onClick={() => onDelete(todo.id)}>删除</button>

这里 onDelete 是父组件通过 props 传进来的方法。转写稿里也强调过,函数组件没有 this,不需要写 this.props,直接使用参数接收到的 props 即可。

易错点:onClick={() => onDelete(todo.id)} 不能写成 onClick={onDelete(todo.id)};id 的变量名要和父组件过滤时一致;如果列表项用 key,建议用 todo.id,不要用索引。

删除该行:子传父 ​

为什么需要:React 是单向数据流,子组件不能直接改父组件的 todos。删除动作由子组件触发,但真正修改状态要在父组件完成,这就是“子传父”。

怎么想:父组件把“删除方法”作为属性传给子组件;子组件点击按钮时调用这个方法,并传入 id;父组件收到 id 后,用新数组替换旧数组。

怎么做:父组件最小示例:

jsx
const [todos, setTodos] = useState([
  { id: 1, text: '学习 React' },
  { id: 2, text: '写 todo' }
])

function handleDelete(id) {
  setTodos(todos.filter(todo => todo.id !== id))
}

<TodoItem todo={todo} onDelete={handleDelete} />

子组件:

jsx
function TodoItem({ todo, onDelete }) {
  return (
    <li>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>删除</button>
    </li>
  )
}

转写稿中验证过:set 方法执行后会触发函数组件重新调用,页面重新渲染。所以这里只要 setTodos 得到一个新数组,列表就会刷新。

易错点:不要用 todos.splice(...) 直接改原数组;filter 会返回新数组,适合删除;父组件传的方法名要和子组件接收的 props 名一致;如果出现 useState is not defined,要检查是否从 React 中导入了 useState。

数据在谁身上,方法就定义在谁身上 ​

为什么需要:这是本节笔记的核心原则。数据在父组件,删除方法就必须定义在父组件;否则子组件没有权限改父状态,逻辑会绕。

怎么想:先判断 todos 在哪个组件里。谁持有 todos,谁就负责 setTodos;子组件只接收 onDelete 并调用。

怎么做:父组件持有 todos,定义 handleDelete;子组件只写 onClick={() => onDelete(todo.id)}。删除时用 filter 过滤掉当前 id。

易错点:不要把删除方法写在子组件里再去改父数据;不要直接给 props 赋值;不要漏传 id,否则父组件不知道删哪一行。

调试与验证 ​

为什么需要:删除功能涉及父子传参、事件回调、状态更新,任何一环名字写错都会“点了没反应”。

怎么想:按链路排查:按钮有没有触发 onClick → id 有没有传对 → 父组件有没有收到 → filter 条件是否正确 → setTodos 是否执行。

怎么做:可在子组件点击处打印 todo.id,在父组件 handleDelete 里打印收到的 id,再检查 filter(todo => todo.id !== id)。转写稿中调试 useState 时也用过打印和刷新观察执行流程,这里同理。

易错点:id 类型不一致会导致过滤失败;onDelete 拼写不一致会拿不到方法;删除后状态没变,要检查是否返回了新数组。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. 首屏渲染 + 添加 todo + 删除 todo(完整可运行) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>TodoList:首屏渲染 / 添加 / 删除</title>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { useState, useRef } = React;

    // 生成不重复 id(笔记方式二:36 进制随机串;也可 npm i nanoid 后 import { nanoid } from 'nanoid')
    const createId = () => Math.random().toString(36).slice(2);

    /* ---------- Header:负责「输入 + 回车」,把内容交给父组件 ---------- */
    function Header({ onAdd }) {
      const inputRef = useRef(null);

      const handleKeyUp = (e) => {
        if (e.key !== 'Enter') return;          // 只处理回车
        const value = inputRef.current.value.trim();
        if (!value) return;                     // 空内容不添加
        onAdd(value);                           // 子传父:把内容交给 TodoList
        inputRef.current.value = '';            // 清空输入框
      };

      return (
        <header>
          <h1>todos</h1>
          <input
            ref={inputRef}
            type="text"
            placeholder="输入内容后按回车"
            onKeyUp={handleKeyUp}
          />
        </header>
      );
    }

    /* ---------- Item:一行 todo,点击删除时把 id 传给父组件 ---------- */
    function Item({ todo, onDel }) {
      return (
        <li>
          <label>{todo.name}</label>
          <button onClick={() => onDel(todo.id)}>删除</button>
        </li>
      );
    }

    /* ---------- Main:接收 todos,map 渲染列表 ---------- */
    function Main({ todos, onDel }) {
      return (
        <ul>
          {todos.map((todo) => (
            <Item key={todo.id} todo={todo} onDel={onDel} />
          ))}
        </ul>
      );
    }

    /* ---------- Footer:接收 todos,计算 total 和 doneNum ---------- */
    function Footer({ todos }) {
      const total = todos.length;
      const doneNum = todos.filter((todo) => todo.done).length;
      return (
        <footer>
          总数:{total} / 已完成:{doneNum}
        </footer>
      );
    }

    /* ---------- TodoList:数据在谁身上,方法就定义在谁身上 ---------- */
    function TodoList() {
      const [todos, setTodos] = useState([
        { id: createId(), name: '吃饭', done: false },
        { id: createId(), name: '睡觉', done: true },
      ]);

      // 添加
      const addTodo = (name) => {
        setTodos([...todos, { id: createId(), name, done: false }]);
      };

      // 删除
      const delTodo = (id) => {
        setTodos(todos.filter((todo) => todo.id !== id));
      };

      return (
        <div>
          <Header onAdd={addTodo} />
          <Main todos={todos} onDel={delTodo} />
          <Footer todos={todos} />
        </div>
      );
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<TodoList />);
  </script>
</body>
</html>

关键行与易错点

  • useState([...]):把 todos 定义成 TodoList 组件的状态(首屏数据),只有状态变化才会触发重新渲染。
  • 状态通过标签属性往下传:<Main todos={todos} />、<Footer todos={todos} />,子组件用 props 解构接收({ todos })。
  • todos.map(...) 渲染列表,key={todo.id} 必须用不重复的 id;用数组下标当 key 在增删时会错位。
  • onAdd(value)、onDel(todo.id) 都是子传父:父组件把方法以属性形式传给子组件,子组件调用时把数据回传。
  • 添加用 [...todos, 新项]、删除用 todos.filter(...),都是生成新数组;直接 todos.push() 不会触发重新渲染。
  • e.key !== 'Enter' 用来过滤按键;inputRef.current.value.trim() 后要判空,否则会加进去一条空 todo。

2. useRef 获取输入框里的内容(添加 todo 的技术点) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>useRef 获取真实 DOM</title>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { useRef } = React;

    function App() {
      // 1. 创建 ref 对象
      const inputRef = useRef();

      const getValue = () => {
        console.log(inputRef);              // 一个对象,里面有 current
        console.log(inputRef.current);      // 真实的 input DOM
        console.log(inputRef.current.value); // 用户输入的内容
      };

      return (
        <div>
          {/* 2. 把 ref 绑定到元素身上 */}
          <input ref={inputRef} type="text" placeholder="请输入内容" />
          <button onClick={getValue}>获取</button>
        </div>
      );
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

关键行与易错点

  • useRef() 也要从 React 里解构出来,和 useState 一样是 hook,不能写在 if / 循环里。
  • 真实 DOM 要通过 inputRef.current 拿,不是 inputRef.value。
  • ref={inputRef} 只是绑定,不要在 JSX 里写 ref={inputRef.current}。
  • 用 ref 取值是「非受控」写法,取的是当下真实值;若用受控组件则要写 value={xxx} onChange={...},笔记里列了这两种技术点,按需选择。

3. useState 基础:状态变化才重新渲染 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>useState 基础</title>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>

  <script type="text/babel">
    const { useState } = React;

    function App() {
      // 解构:前面是状态数据,后面是设置状态的方法
      const [count, setCount] = useState(99);

      console.log('App 渲染了一次');   // 每渲染一次就打印一次

      return (
        <div>
          <h2>{count}</h2>
          <button onClick={() => setCount(count + 1)}>count + 1</button>
          <button onClick={() => setCount(9999)}>设置固定值 9999</button>
        </div>
      );
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>

关键行与易错点

  • useState(99) 返回的是数组,要按位置解构:const [状态, 设置状态的方法] = useState(初始值),名字可以随便起,但一般叫 xxx / setXxx。
  • 直接写 count,函数组件里没有 this,不要写 this.state.count。
  • 调用 setCount 会做两件事:把参数作为最新状态,并触发函数组件重新调用(页面重新渲染),所以 console.log 会再次打印。
  • 传固定值 9999 时,从 9999 再点 9999 只会多渲染一次、之后不再渲染,这是 useState 对状态没改变做的优化,和类组件不同。
  • 不要在 setCount 里直接改原值(如 count++ 后再传),要传新的值,如 setCount(count + 1)。

常见错误 ​

  1. 现象:点击按钮后紧接着 console.log(count),打印的还是旧值,页面却已经更新。 原因:函数组件每次渲染都有一份独立的变量快照,setCount 只是登记下一次渲染,不会修改当前作用域里的变量。 修法:用 setCount(c => c + 1) 这种函数式写法算新值;确实需要“渲染后的值”,就把读取逻辑放进 useEffect。

  2. 现象:控制台报 useState is not defined / useEffect is not defined,或者提示 Hook 只能在函数组件内调用。 原因:Hook 是 react 的具名导出,忘记 import;或者把 Hook 写进了 if、循环、普通函数里。 修法:import { useState, useEffect, useRef } from 'react',并保证所有 Hook 都在组件函数的最顶层、每次渲染都按同样顺序执行。

  3. 现象:电子时钟反复点“显示/隐藏”后,秒数跳得越来越快,像有多个定时器在跑。 原因:useEffect 里 setInterval 只开了没关,组件卸载后旧定时器仍然存活。 修法:在 useEffect 回调里 return () => clearInterval(timer),把清理动作和开启动作写在同一个 effect 里。

  4. 现象:定时器或事件回调里读到的 count 永远是初始值 0。 原因:依赖数组写成了空数组,effect 里的闭包捕获的是首次渲染的那份变量,之后不再更新。 修法:把用到的状态加进依赖数组,让 effect 重建;或者改用函数式更新 / useRef 保存最新值。

  5. 现象:列表删除时删错行,或者输入框内容在行与行之间“串”。 原因:key 用了 index 或 Math.random(),每次渲染 key 都在变或与数据对不上,React 认错了行。 修法:新增 todo 时就用 nanoid() 生成 id 存进对象,渲染和删除一律用 todo.id。

  6. 现象:子组件里直接 todos.push(...) 或 todos.splice(...),页面不刷新甚至报错。 原因:props 是只读的,数据源在父组件;而且原地修改数组引用不变,React 认为没变化。 修法:子组件只负责把 id 通过回调传出去,父组件用 setTodos(todos.filter(t => t.id !== id))、[...todos, newTodo] 生成新数组。

  7. 现象:受控输入框敲不进字,或者控制台警告 value 没有配 onChange。 原因:只写了 value={text} 没写 onChange,输入框被锁死成只读。 修法:补上 onChange={e => setText(e.target.value)};若只想回车提交、不想每次输入都渲染,就改用 useRef 取值。

练习 ​

  1. 首屏渲染:把 todos 提到 TodoList 的状态里,预置 3 条数据(id 用 nanoid() 生成),通过属性分别传给 Main 和 Footer。Main 用 map 渲染并给 Item 传单条 todo,Footer 算出 total 和 doneNum 后渲染。要求:渲染前先 console.log 一次 todos,确认数据结构。

  2. 添加 todo:用受控组件获取输入,onKeyUp 里判断 e.key === 'Enter' 后提交,trim() 后为空则不添加。完成后把受控写法改成 useRef 写法,对比两种方式各自在“输入时会不会触发重渲染”上的差别。

  3. 删除 todo:在 Item 里给删除按钮绑 onClick,把 todo.id 传给父组件,父组件用 filter 生成新数组。做完后再加一个“清除已完成”按钮,要求复用同一套“子传父 + 父改数据”的思路。

  4. 电子时钟:Timer 组件里用 useState 存当前时间,useEffect 里开 setInterval 并 return 清理函数。父组件用奇偶数控制 Timer 的条件渲染,切换时观察控制台打印顺序,说出哪一行对应挂载、哪一行对应卸载。

  5. useRef:给一个 input 绑 ref,放两个按钮——“聚焦”和“取值”。点击后分别调用 inputRef.current.focus() 和读取 inputRef.current.value。思考:不用受控组件也能拿到用户输入,那什么时候必须用受控、什么时候 ref 就够?

原视频对照 ​

#视频时长
1useState14:35
2useState-深入理解12:57
3函数组件状态不改变只多渲染一次07:16
4useEffect基本使用20:08
5电子时钟练习05:55
6useRef03:02

小结 ​

  1. Hook 不是新能力,而是把类组件的三件套(state、生命周期、ref)换成了一套更短的语法,能力等价、写法更轻。
  2. useState 的本质是“本次渲染的快照 + 一个登记器”,setXxx 不修改当前变量,只排队下一次渲染。想通这一句,前面大半错误都能自己定位。
  3. useState 对相同值做了浅比较优化:赋同样的值只多渲染一次,之后不再渲染。但这是性能优化,不能拿它当副作用拦截器用。
  4. useEffect 的依赖数组就是“这段逻辑关心谁”:空数组=只在挂载时跑一次;写了变量=该变量变才跑;不写=每次渲染都跑;return 的函数=卸载清理。
  5. 一个函数组件可以写多个 useEffect,各管一件事(开定时器、监听某个值、清理监听),比类组件把逻辑全塞进一个 componentDidMount 更好拆、更好删。
  6. 数据在谁身上,修改方法就定义在谁身上;子组件只负责“报告发生了什么”(传 id 上去),父组件负责“改数据”。这条决定了添加和删除能不能一次写对。
  7. key 是 React 认行的唯一凭据,稳定 id 必须在数据生成的那一刻就产生并跟着数据走,不要在渲染时临时造。
  8. 函数组件没有 this,状态和 ref 都是解构出来的局部变量,命名上保持 xxx / setXxx 的对应关系,能省掉大量“这变量是哪来的”的排查时间。