React 第3周笔记-列表渲染 -表单-事件处理

路线图阶段:第一阶段(React 基础)第3周 项目:Todo List(添加 / 删除 / 标记完成) 核心心智模型:UI = f(state) —— 状态变,UI 自动跟着变


1. 列表渲染:.map() + key

把数组变成一组 UI 元素。

<ul>
  {list.map((item) => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

key 是什么、为什么需要

小括号 vs 大括号(箭头函数返回值)

// 写法1:() 包住 → 自动 return(常用于多行 JSX)
(item) => (
  <li>...</li>
)

// 写法2:{} 是函数体 → 必须手动 return
(item) => {
  return <li>...</li>
}

⚠️ 小括号的作用是”让多行 JSX 自动被 return”,和”返回对象”无关


2. 受控输入框(Controlled Input)

输入框的值存在 state 里,由 React 全权管理。

const [todo, setTodo] = useState("");

function handleChange(e) {
  setTodo(e.target.value); // e.target 是 input 元素,.value 是它当前的值
}

<input type="text" value={todo} onChange={handleChange} />

关键点

受控输入框的好处


3. 不可变更新(Immutable Update)

React 的数组/对象不能直接改,要创建新的再 setState。

添加:展开运算符 ...

function handleAdd() {
  const newItem = { id: Date.now(), name: todo, finish: false };
  setList([...list, newItem]); // 复制旧数组 + 新项 → 全新数组
  setTodo("");                 // 顺手清空输入框
}

❌ 不要用 list.push(...)(会直接改原数组)

删除:.filter()

function handleDelete(id) {
  const newList = list.filter((item) => item.id !== id); // 只保留 id 不等于目标的
  setList(newList);
}

修改某一项:.map() + 取反

function handleToggle(id) {
  const newList = list.map((item) =>
    item.id === id ? { ...item, finish: !item.finish } : item
  );
  setList(newList);
}

4. 事件处理:箭头函数包裹

需要传参数时,必须用箭头函数包一层。

// ❌ 带括号 = 渲染时立刻执行(还没点就跑了)
<button onClick={handleDelete(item.id)}>删除</button>

// ✅ 箭头函数包裹 = 交给 React 一个待命函数,点击时才执行
<button onClick={() => handleDelete(item.id)}>删除</button>

// 不传参时,直接给函数名即可(不要括号)
<button onClick={handleAdd}>Add</button>

一句话:onClick 要的是”一个待命的函数”,不是”函数的执行结果”。


5. 事件冒泡(Event Bubbling)+ stopPropagation

点击事件会从内往外冒泡(像往水里扔石头的涟漪)。 按钮在 <li> 里 → 点按钮会同时触发按钮和 li 的 onClick。

<li onClick={() => handleToggle(item.id)}>
  <p>{item.name}</p>
  <button onClick={(e) => {
    e.stopPropagation(); // 在内层(button)就拦住,不让冒泡到外层 li
    handleDelete(item.id);
  }}>删除</button>
</li>

⚠️ stopPropagation() 必须加在内层(button),不能加在外层(li)—— 因为要在事件离开 button、到达 li 之前拦截。


6. 派生状态(Derived State):不要乱加 useState

什么时候才需要新的 useState?同时满足两条:

  1. 会随用户交互而改变
  2. 没法从现有 state 算出来
// finish 是数据源(source of truth),存在 state 里
// 删除线只是它的"视觉呈现",直接算,不要单独存
<p style={{ textDecoration: item.finish ? "line-through" : "none" }}>
  {item.name}
</p>

7. JSX 行内样式(Inline Style)

style={{ textDecoration: "line-through", backgroundColor: "red" }}

完整代码:Todo List

import { useState } from "react";

function App() {
  const [todo, setTodo] = useState("");
  const [list, setList] = useState([]);

  function handleChange(e) {
    setTodo(e.target.value);
  }

  function handleAdd() {
    const newItem = { id: Date.now(), name: todo, finish: false };
    setList([...list, newItem]);
    setTodo("");
  }

  function handleDelete(id) {
    const newList = list.filter((item) => item.id !== id);
    setList(newList);
  }

  function handleToggle(id) {
    const newList = list.map((item) =>
      item.id === id ? { ...item, finish: !item.finish } : item
    );
    setList(newList);
  }

  return (
    <>
      <input type="text" value={todo} onChange={handleChange} />
      <button onClick={handleAdd}>Add</button>
      <ul>
        {list.map((item) => (
          <li key={item.id} onClick={() => handleToggle(item.id)}>
            <p style={{ textDecoration: item.finish ? "line-through" : "none" }}>
              {item.name}
            </p>
            <button onClick={(e) => {
              e.stopPropagation();
              handleDelete(item.id);
            }}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

export default App;

下一步(第4周预告)