目 录CONTENT

文章目录

React(十)useState

React(十):useState 全方位指南 —— 函数组件的"记忆细胞"

目录


第一部分:基础定义 —— useState 到底是什么?


1.1 生活化类比:带更新通知的便签本

想象你在厨房做菜,你需要记住"盐放了多少克"。你随手拿了一张便签纸,在上面写下 盐 = 5g,然后贴在冰箱门上。

接下来发生的事情:

  • 你放了一次盐,把便签纸上的数字划掉,写上 盐 = 8g
  • 但神奇的是——便签纸上的字一变,灶台上的显示屏自动刷新,实时告诉你"当前已放盐 8g"
  • 如果你直接用手擦便签纸,显示屏不会更新——你必须通过便签纸配套的专用笔去改写,显示屏才会跟着变

React 里的 useState 就是这样一套机制:

厨房场景 React 中的对应
便签纸 state 变量(存储当前值)
便签纸上的内容 状态值(比如 5
专用笔 setState 更新函数(唯一合法的修改入口)
灶台显示屏自动刷新 React 自动重渲染组件
手擦便签纸不更新 直接修改 state 不触发渲染
// 就像你在厨房里准备了一张便签纸和一支专用笔
const [salt, setSalt] = useState(5); // 便签纸初始写的是 5g

// 想改?只能用专用笔写
setSalt(8); // React 检测到变化 → 灶台显示屏自动刷新

一句话总结useState 给函数组件配了一本"带自动通知功能的便签本"。你在便签本上写东西(state),React 帮你在每次内容变化时自动刷新 UI(重新渲染)。


1.2 useState 的核心作用

用最直白的话说:useState 是 React 官方提供的 Hook,它让函数组件拥有了"记忆能力"。

没有 useState 的时候,函数组件就像一台没有硬盘的电脑——每次开机(重新渲染)都从零开始,上一次的运行结果全部丢失。有了 useState,函数组件就有了"记忆细胞",可以在多次渲染之间保持数据。

useState 解决的核心问题:

  • 函数组件没有 this:普通函数执行完就销毁了,下次渲染时所有的局部变量都会重新初始化。useState 把数据"寄存"在 React 内部,让它跨越渲染周期存活。
  • 数据变化需要触发 UI 更新:普通变量改了 React 不知道,但 setState 一调用,React 就会自动调度一次重渲染。

1.3 对比类组件的 this.state

在 Hooks 出现之前,函数组件是"无状态"的,想要状态只能用类组件。我们对比一下:

类组件时代(旧写法):

class Counter extends React.Component {
  constructor(props) {
    super(props);
    // 所有状态挤在一个对象里
    this.state = {
      count: 0,
      name: "",
    };
  }

  increment = () => {
    // 通过 setState 合并更新
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>计数:{this.state.count}</p>
        <button onClick={this.increment}>+1</button>
      </div>
    );
  }
}

函数组件 + useState(现代写法):

function Counter() {
  // 每个状态独立声明,职责清晰
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
}

核心差异对比:

维度 类组件 this.state 函数组件 useState
状态组织方式 所有状态塞在一个大对象里 每个状态独立声明,按需拆分
更新方式 this.setState 自动浅合并 setState 直接替换整个 state
读取最新值 this.state.xxx 通过 state 变量直接读取
代码量 constructor + bind + render 一行 Hook 搞定
逻辑复用 HOC / render props(复杂) 自定义 Hook(简洁)

新手重点提醒:类组件的 setState 会自动把新对象合并到旧 state 中;而 useState 的 setter 不会自动合并——你传什么,它就整个替换成什么。这是新手最容易出错的差异点。


1.4 API 签名详解

const [state, setState] = useState(initialValue);

逐部分拆解:

部分 说明
useState React 内置 Hook,从 react 包中导入
initialValue 状态的初始值,只在组件首次渲染时使用。可以是任意类型:数字、字符串、对象、数组、函数等
state 当前的状态值(只读)
setState 更新状态的函数。调用它并传入新值,React 会调度重渲染
[state, setState] 数组解构语法,名字可以任意取,但业界约定为 [xxx, setXxx]

参数类型签名:

// 简化版 TypeScript 签名
function useState<S>(
  initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>];

// 你也可以显式指定泛型
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);

React 19 特别说明:React 18 / 19 中的 useState API 签名与行为没有本质变化。React 18 引入的自动批量更新(Automatic Batching)在 React 19 中继续生效且表现一致。文档中所有示例同时适用于 React 18 和 React 19。


第二部分:基础使用 —— 三步上手 useState


2.1 完整使用步骤拆解

把 useState 的使用拆成五步,每一步都搞清楚:

第一步:从 react 中导入

import { useState } from "react";

第二步:在函数组件内部(顶层)声明状态

function MyComponent() {
  const [name, setName] = useState(""); // 声明一个字符串状态
  //     ↑       ↑            ↑
  //   变量名  更新函数名   初始值(空字符串)

  // ... 组件其余逻辑
}

第三步:在 JSX 中使用 state 变量

return (
  <div>
    <p>当前名字:{name}</p> {/* 读取状态值,显示在 UI 中 */}
  </div>
);

第四步:通过事件或其他时机调用 setState 更新

const handleChange = (e) => {
  setName(e.target.value); // 用户输入 → 更新状态
};

第五步:React 检测到变化 → 自动重渲染

用户输入 "小" → setName("小") → React 标记该组件需要更新
→ React 用新状态重新执行函数组件 → name 变成 "小"
→ JSX 中的 {name} 渲染出 "小" → 用户看到变化

首次渲染执行流程

  1. React 调用 MyComponent() 函数
  2. 执行到 useState(""),React 检查:这个组件是第一次渲染吗?是 → 用初始值 ""
  3. name 被赋值为 "",JSX 渲染出空的 <p>当前名字:</p>
  4. React 将 state 存入内部存储,记住这个组件有一个 state,索引为 0

2.2 初始值的两种设置方式

方式一:直接传固定值(最常见)

const [count, setCount] = useState(0); // 数字
const [name, setName] = useState("小明"); // 字符串
const [isOn, setIsOn] = useState(false); // 布尔
const [list, setList] = useState([]); // 空数组
const [user, setUser] = useState(null); // null

适合场景:初始值是一个简单常量,计算成本为零。

方式二:懒初始化函数(惰性初始化)

// ❌ 不好的写法:每次渲染都会执行 expensiveComputation()
const [data, setData] = useState(expensiveComputation());

// ✅ 好的写法:传入函数,React 只在首次渲染时调用它
const [data, setData] = useState(() => expensiveComputation());

为什么需要懒初始化?

// 假设这个计算很耗时(比如从 localStorage 读大量数据并解析)
function readUserSettings() {
  console.log("正在读取用户设置...(耗时的计算)");
  const raw = localStorage.getItem("userSettings");
  return raw ? JSON.parse(raw) : { theme: "light", fontSize: 14 };
}

function App() {
  // ❌ 每次渲染都执行 readUserSettings(),浪费性能
  const [settings, setSettings] = useState(readUserSettings());

  // ✅ 传入函数,React 只在首次渲染时调用一次
  const [settings, setSettings] = useState(() => readUserSettings());
  // 等效于:const [settings, setSettings] = useState(readUserSettings);
  // 区别在于:前者传入函数引用,React 会帮你只在初始化时调用
}

温馨提示:如果你看到 useState(computeSomething()) 而不是 useState(() => computeSomething()),则 computeSomething()每次渲染时都会执行(只是除了首次外,结果会被丢弃)。对于轻量计算无所谓,但对于读 localStorage、复杂计算等场景,务必使用函数形式。


2.3 示例一:计数器(最简入门)

目标:实现一个点击按钮数字 +1 的计数器。

import { useState } from "react";

function Counter() {
  // 1. 声明状态:count 初始值为 0
  const [count, setCount] = useState(0);

  // 2. 定义更新逻辑:每次 +1
  const handleIncrement = () => {
    setCount(count + 1); // 基于当前值计算新值
  };

  // 3. 渲染:在 JSX 中展示 count,按钮绑定点击事件
  return (
    <div>
      <h2>计数器示例</h2>
      <p>
        当前计数:<strong>{count}</strong>
      </p>
      <button onClick={handleIncrement}>点我 +1</button>
    </div>
  );
}

执行流程说明:

初始渲染:
  React 调用 Counter()
  → useState(0) 返回 [0, setCount]
  → count = 0,渲染出 "当前计数:0"

用户点击按钮:
  → handleIncrement() 执行
  → setCount(0 + 1) → setCount(1)
  → React 标记 Counter 需要重渲染

第二次渲染:
  React 再次调用 Counter()
  → useState(0) —— React 发现这不是首次渲染,忽略参数 0,用已存储的值 1
  → count = 1,渲染出 "当前计数:1"

用户再点按钮:
  → setCount(1 + 1) → setCount(2)
  → 渲染出 "当前计数:2" ... 以此类推

2.4 示例二:文本输入框同步(受控组件)

目标:一个输入框,输入什么就实时显示什么,同时有一个清空按钮。

import { useState } from "react";

function NameInput() {
  // 声明状态:用于存储输入框的值
  const [text, setText] = useState("");

  // 输入框变化时,同步更新状态
  const handleChange = (e) => {
    // e.target.value 是输入框的当前文字
    setText(e.target.value);
  };

  // 清空输入框
  const handleClear = () => {
    setText("");
  };

  return (
    <div>
      <h2>文本输入框同步</h2>

      {/* input 的 value 绑定 state,onChange 更新 state */}
      <input
        type="text"
        value={text} // ← 输入框显示的内容 = state
        onChange={handleChange} // ← 用户输入 → 更新 state
        placeholder="请输入你的名字"
      />

      {/* 实时显示输入内容 */}
      <p>
        你输入的是:<strong>{text || "(暂无内容)"}</strong>
      </p>

      {/* 清空按钮 */}
      <button onClick={handleClear}>清空</button>
    </div>
  );
}

"受控组件"是什么意思?

普通 HTML input:用户敲键盘 → 浏览器自己管理输入框里的文字
React 受控 input:用户敲键盘 → onChange 事件 → setState → React 重渲染 → input 用 value 重新显示

                  用户输入 "a"
                       ↓
              onChange 触发
                       ↓
          setText("a")  ← React 知道值变了
                       ↓
          组件重渲染
                       ↓
      input value="a"  ← React 控制显示

这就是"受控"——input 的值被 React state 牢牢控制,而非浏览器自己管。

2.5 示例三:开关切换(布尔状态)

目标:一个开关按钮,点击切换开/关状态,并展示对应的文字和样式。

import { useState } from "react";

function Toggle() {
  // 布尔类型的状态:true 表示开,false 表示关
  const [isOn, setIsOn] = useState(false);

  // 切换函数:取反
  const handleToggle = () => {
    setIsOn(!isOn); // 依赖于当前值取反
    // 更安全的写法(见进阶章节):
    // setIsOn(prev => !prev);
  };

  return (
    <div>
      <h2>开关切换示例</h2>

      {/* 根据 isOn 的值展示不同内容 */}
      <p>
        当前状态:
        <strong style={{ color: isOn ? "green" : "red" }}>
          {isOn ? "开" : "关"}
        </strong>
      </p>

      {/* 按钮文字和样式也随状态变化 */}
      <button
        onClick={handleToggle}
        style={{
          padding: "10px 24px",
          backgroundColor: isOn ? "#4caf50" : "#f44336",
          color: "white",
          border: "none",
          borderRadius: "4px",
          cursor: "pointer",
          fontSize: "16px",
        }}
      >
        {isOn ? "关闭" : "打开"}
      </button>
    </div>
  );
}

布尔状态的常见应用场景:

场景 状态名示例 初始值
开关 / 切换 isOn false
模态框显隐 isOpen false
加载中 isLoading true
是否已提交 isSubmitted false
显示/隐藏密码 showPassword false

第三部分:进阶场景 —— 玩转复杂状态


3.1 对象类型的状态管理

状态不限于数字和字符串,对象也很常见。但更新对象时有一个铁律:必须遵循不可变更新原则。

核心原则:不可变更新(Immutable Update)

❌ 错误思路:直接修改原对象,然后传给 setState
✅ 正确思路:创建一个新对象(拷贝 + 修改),然后传给 setState

为什么必须这样? React 通过引用来判断状态是否变化。如果你直接修改原对象,引用没变,React 认为"状态没变",不会重渲染。

正反对比示例:用户信息表单

import { useState } from "react";

function UserProfile() {
  const [user, setUser] = useState({
    name: "小明",
    age: 20,
    city: "北京",
  });

  // ❌ 错误写法:直接修改原对象
  const handleWrongUpdate = () => {
    user.age = 21; // 直接修改了原对象!
    setUser(user); // 引用没变,React 认为没变化,不会重渲染!
  };

  // ✅ 正确写法一:展开运算符创建新对象
  const handleCorrectUpdate = () => {
    setUser({
      ...user, // 拷贝所有旧属性
      age: 21, // 覆盖需要修改的属性
    });
  };

  // ✅ 正确写法二:使用更新函数(当新值依赖旧值时推荐)
  const handleUpdateWithFunction = () => {
    setUser((prevUser) => ({
      ...prevUser,
      age: prevUser.age + 1,
    }));
  };

  // ✅ 修改单个字段的通用处理函数
  const handleFieldChange = (field, value) => {
    setUser((prev) => ({
      ...prev,
      [field]: value, // 动态属性名
    }));
  };

  return (
    <div>
      <h2>用户信息</h2>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <p>城市:{user.city}</p>

      <button onClick={handleCorrectUpdate}>年龄 +1(正确)</button>

      <input
        value={user.city}
        onChange={(e) => handleFieldChange("city", e.target.value)}
        placeholder="修改城市"
      />
    </div>
  );
}

新手重点提醒{ ...user, age: 21 } 这种写法叫"浅拷贝"。如果对象内部有嵌套对象,嵌套对象的引用仍然是旧的。对于深层嵌套,需要用 { ...user, address: { ...user.address, city: "上海" } } 这种逐层展开的方式,或使用 useImmer 等工具库简化。


3.2 数组类型的状态管理

数组更新的不可变原则与对象一样。好消息是 JavaScript 数组本身就有很多"返回新数组"的方法。

数组操作速查表

操作 ❌ 错误(修改原数组) ✅ 正确(返回新数组)
添加 arr.push(x) [...arr, x]
删除 arr.splice(i, 1) arr.filter((_, idx) => idx !== i)
修改 arr[i] = x arr.map((item, idx) => idx === i ? x : item)
插入 arr.splice(i, 0, x) [...arr.slice(0, i), x, ...arr.slice(i)]
排序 arr.sort() [...arr].sort()
反转 arr.reverse() [...arr].reverse()
清空 arr.length = 0 []

正反对比示例:学生名单管理

import { useState } from "react";

function StudentList() {
  const [students, setStudents] = useState(["张三", "李四"]);
  const [newName, setNewName] = useState("");

  // ❌ 错误:直接 push 修改原数组
  const addWrong = () => {
    students.push(newName); // 直接修改了原数组!
    setStudents(students); // 引用没变,不会重渲染
  };

  // ✅ 正确:展开运算符创建新数组
  const addCorrect = () => {
    if (newName.trim() === "") return;
    setStudents([...students, newName]); // 全新数组
    setNewName(""); // 清空输入框
  };

  // ✅ 删除:filter 返回新数组
  const removeStudent = (index) => {
    setStudents(students.filter((_, i) => i !== index));
    // 或者用更新函数形式(更推荐):
    // setStudents(prev => prev.filter((_, i) => i !== index));
  };

  // ✅ 修改:map 返回新数组
  const updateStudent = (index, newValue) => {
    setStudents(students.map((name, i) => (i === index ? newValue : name)));
  };

  return (
    <div>
      <h2>学生名单</h2>

      {/* 添加区域 */}
      <input
        value={newName}
        onChange={(e) => setNewName(e.target.value)}
        placeholder="输入新学生姓名"
      />
      <button onClick={addCorrect}>添加</button>

      {/* 列表展示 */}
      <ul>
        {students.map((name, index) => (
          <li key={index}>
            {name}
            <button onClick={() => removeStudent(index)}>删除</button>
            <button
              onClick={() => {
                const newName = prompt("修改姓名", name);
                if (newName) updateStudent(index, newName);
              }}
            >
              修改
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

3.3 多状态的声明与管理

当一个组件需要多个状态时,有两种组织方式:

方式一:多个独立 useState(推荐)

function Form() {
  // 各自独立,互不干扰
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState(18);

  // 更新互不影响
  return (
    <form>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
      />
    </form>
  );
}

优点:每个状态独立更新,互不干扰,逻辑清晰。
缺点:字段特别多时(比如 10 个以上),useState 声明会比较多。

方式二:一个 useState 管理对象(适合关联性强的字段)

function Form() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    age: 18,
  });

  // 通用更新函数
  const handleChange = (field, value) => {
    setFormData((prev) => ({
      ...prev,
      [field]: value,
    }));
  };

  return (
    <form>
      <input
        value={formData.name}
        onChange={(e) => handleChange("name", e.target.value)}
      />
      <input
        value={formData.email}
        onChange={(e) => handleChange("email", e.target.value)}
      />
      <input
        type="number"
        value={formData.age}
        onChange={(e) => handleChange("age", Number(e.target.value))}
      />
    </form>
  );
}

优点:一次 setFormData 可以更新多个字段。
缺点:任何字段更新都要注意不可变拷贝,容易忘写 ...prev

选择建议

场景 推荐方式
字段之间相互独立(如多个开关) 多个独立 useState
字段属于同一实体(如用户信息) 一个对象 useState
字段需要一起提交(如表单) 一个对象 useState
字段的更新频率差异大 多个独立 useState

3.4 状态更新的异步批量更新特性

什么是"批量更新"?

React 为了性能,会把同一个事件处理函数中的多次 setState合并成一次更新,只触发一次渲染。

function BatchDemo() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1); // 希望 +1
    setCount(count + 1); // 希望再 +1
    setCount(count + 1); // 希望再 +1
    // 问题:点击后 count 变成多少?
  };

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={handleClick}>+3</button>
    </div>
  );
}

答案:count 变成 1,不是 3。

为什么?因为这三个 setCount(count + 1) 在同一个事件处理中执行时,count 的值是同一个快照(都是 0)。所以每条语句等价于 setCount(0 + 1)

解决方案:使用更新函数(Updater Function)

const handleClick = () => {
  setCount((prev) => prev + 1); // prev = 0 → 1
  setCount((prev) => prev + 1); // prev = 1 → 2
  setCount((prev) => prev + 1); // prev = 2 → 3
  // 结果:count = 3
};

当传给 setState 的是一个函数时,React 会把这个函数加入更新队列,并保证每次调用时 prev 都是最新的值

批量更新的实际案例

function BatchExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleClick = () => {
    // 两个不同状态的 setState 也会被批量处理
    setCount((c) => c + 1); // 更新 1
    setFlag((f) => !f); // 更新 2
    // React 18/19:这两次更新合并 → 只触发一次渲染
    // React 17 及以前:在 setTimeout/promise 中不会合并
  };

  console.log("渲染了,count =", count, "flag =", flag);

  return (
    <div>
      <p>count: {count}</p>
      <p>flag: {String(flag)}</p>
      <button onClick={handleClick}>同时更新两个状态</button>
    </div>
  );
}

React 18/19 自动批量更新规则

代码位置 React 17 React 18/19
事件处理函数中(onClick 等) 批量 ✅ 批量 ✅
setTimeout / setInterval 回调中 不批量 批量 ✅
Promise.then / async/await 中 不批量 批量 ✅
原生事件监听中 不批量 批量 ✅

一句话记住:React 18 及以后,任何情况下多个 setState 都会自动批量处理。当新值依赖旧值时,一律使用更新函数 setXxx(prev => ...) 最安全。


第四部分:教学专属注意事项 —— 新手避坑指南


4.1 不能在条件/循环/嵌套函数中调用 useState

错误示例

function BadComponent({ isLoggedIn }) {
  // ❌ 条件判断中调用 useState
  if (isLoggedIn) {
    const [user, setUser] = useState(null);
  }

  // ❌ 循环中调用 useState
  for (let i = 0; i < 5; i++) {
    const [value, setValue] = useState(i);
  }

  // ❌ 嵌套函数中调用 useState
  const handleSomething = () => {
    const [temp, setTemp] = useState(""); // 不行!
  };

  return <div>...</div>;
}

为什么会出问题?

React 靠 Hook 的调用顺序 来区分不同的 state。每次渲染时,React 按照固定的顺序依次读取 Hook。如果某次渲染跳过了某个 Hook(被 if 包裹),顺序就乱了:

第一次渲染(isLoggedIn = true):
  Hook #0: useState(null)   → user state
  Hook #1: useState(0)      → count state

第二次渲染(isLoggedIn = false):
  Hook #0: useState(0)      → 本应该是 count,但 React 以为这是 user!
  // count 读到了 user 的值,数据错乱!

正确做法

function GoodComponent({ isLoggedIn }) {
  // ✅ 始终在顶层声明所有 Hook
  const [user, setUser] = useState(null);
  const [count, setCount] = useState(0);

  // 条件逻辑放在 Hook 下方
  if (!isLoggedIn) {
    return <div>请先登录</div>;
  }

  return (
    <div>
      欢迎,{user?.name},计数:{count}
    </div>
  );
}

铁律:Hook 必须在函数组件的顶层调用,不能在条件、循环、嵌套函数中使用。 这是 React Hook 的"规则第一条",ESLint 的 eslint-plugin-react-hooks 能帮你自动检测。


4.2 不能直接修改 state,必须用 setState

错误示例 vs 正确示例

function BadVsGood() {
  const [items, setItems] = useState(["a", "b"]);
  const [user, setUser] = useState({ name: "小明", age: 18 });

  // ========== 数组操作 ==========

  // ❌ push 直接修改原数组
  const addWrong = () => {
    items.push("c");
    setItems(items); // 引用没变,不触发渲染!
  };

  // ✅ 创建新数组
  const addCorrect = () => {
    setItems([...items, "c"]);
  };

  // ========== 对象操作 ==========

  // ❌ 直接修改属性
  const updateWrong = () => {
    user.age = 19;
    setUser(user);
  };

  // ✅ 创建新对象
  const updateCorrect = () => {
    setUser({ ...user, age: 19 });
  };

  return (/* ... */);
}

底层逻辑图解

state 在 React 内部存储结构(简化):

  componentFiber.stateHooks = [
    { value: ['a', 'b'] },  ← items 状态
    { value: { name: '小明', age: 18 } }  ← user 状态
  ]

当你调用 setState 时:

  setItems(['a', 'b', 'c'])
  → React 比较:新值 ['a', 'b', 'c'] !== 旧值 ['a', 'b'] ✅ 变了!
  → 触发重渲染

当你直接 push 时:

  items.push('c')  → items 变成 ['a', 'b', 'c']
  setItems(items)  → React 比较:items === items(同一个引用!)❌ 没变!
  → 不触发渲染

React 用的是 Object.is() 比较新旧值,引用类型靠引用地址判断。

4.3 状态更新的异步性

问题场景:setState 后立刻读取 state,拿到的是旧值

function AsyncProblem() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // 期望 1,实际 0!
    // 因为 setCount 是异步调度,count 的值还没变
  };

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={handleClick}>+1 并打印</button>
    </div>
  );
}

三种解决方案

function Solutions() {
  const [count, setCount] = useState(0);

  const handleClick1 = () => {
    // 方案一:使用更新函数,在函数内拿到最新值
    setCount((prev) => {
      const newValue = prev + 1;
      console.log("最新值(方案一):", newValue); // ✅ 拿到正确值
      return newValue;
    });
  };

  const handleClick2 = () => {
    // 方案二:用 useEffect 监听 state 变化
    setCount(count + 1);
    // useEffect 会在渲染完成后执行,此时能拿到新值
  };

  // 配合方案二
  // useEffect(() => {
  //   console.log("count 已更新为:", count);
  // }, [count]);

  const handleClick3 = () => {
    // 方案三:用临时变量记录新值
    const newCount = count + 1;
    setCount(newCount);
    console.log("最新值(方案三):", newCount); // ✅ 直接用临时变量
  };

  return (/* ... */);
}

4.4 初始值只在首次渲染生效

这是一个常见的"为什么我的状态没更新"来源:

function InitialValueTrap({ initialName }) {
  // 即使 initialName 变了,name 也不会变!
  const [name, setName] = useState(initialName);

  console.log("父组件传来的 initialName:", initialName);
  console.log("当前 name 状态:", name);

  return <p>{name}</p>;
}

// 父组件
function Parent() {
  const [name, setName] = useState("小明");
  return (
    <div>
      <button onClick={() => setName("小红")}>改名</button>
      <InitialValueTrap initialName={name} />
      {/* 点击改名后,InitialValueTrap 中的 name 仍然是 "小明"! */}
    </div>
  );
}

原因useState(initialName) 中的 initialName 只在组件首次挂载时被使用。后续重渲染时,React 会忽略这个参数,直接从内部存储中读取已保存的值。

解决方案

function Fixed({ initialName }) {
  const [name, setName] = useState(initialName);

  // 方案一:用 useEffect 同步外部值
  useEffect(() => {
    setName(initialName);
  }, [initialName]);

  // 方案二:如果只是要用 prop 的值,直接用 prop,不需要 state
  // 根本不需要 useState!

  return <p>{name}</p>;
}

判断原则:如果一个值完全由父组件决定(子组件自己不修改它),就不要用 useState 存,直接用 prop。useState 是用来管理组件内部自己产生和变化的数据的。


4.5 避免滥用 useState

能用派生值就不要声明状态

function AbuseExample({ firstName, lastName }) {
  // ❌ 滥用:fullName 完全可以从 firstName 和 lastName 计算出来
  const [fullName, setFullName] = useState("");
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]);

  // ✅ 正确做法:直接计算派生值,不需要 state
  const fullName = `${firstName} ${lastName}`;

  return <p>{fullName}</p>;
}

滥用识别清单

场景 是否需要 useState 正确做法
值可以在渲染时从其他 state/prop 计算出来 ❌ 不需要 直接声明普通变量计算
值不会被 UI 使用、也不影响渲染 ❌ 不需要 使用 useRef
值不需要在多次渲染间保持 ❌ 不需要 声明普通变量
值由父组件传入且子组件不改 ❌ 不需要 直接用 prop
值在组件生命周期中会独立变化并影响 UI ✅ 需要 使用 useState
function SmartVsOverkill() {
  const [firstName, setFirstName] = useState("张");
  const [lastName, setLastName] = useState("三");

  // ✅ 派生值:直接计算
  const fullName = `${firstName}${lastName}`;

  // ✅ 派生值:带逻辑
  const displayName = firstName && lastName ? fullName : "匿名用户";

  // ✅ 派生值:数组操作
  const [list, setList] = useState([1, 2, 3]);
  const count = list.length;
  const hasItems = list.length > 0;

  return (
    <div>
      <p>姓名:{displayName}</p>
      <p>列表项数:{count}</p>
      <p>{hasItems ? "有数据" : "列表为空"}</p>
    </div>
  );
}

第五部分:教学拓展 —— 实战案例与 Hook 配合


5.1 案例一:Todo 列表的增删改查

一个完整的 Todo 列表教学案例,涵盖数组状态的增、删、改、查以及筛选显示。

import { useState } from "react";

function TodoApp() {
  // 1. 任务列表状态:数组,每项是一个 { id, text, done } 对象
  const [todos, setTodos] = useState([
    { id: 1, text: "学习 React useState", done: false },
    { id: 2, text: "完成课后练习", done: false },
  ]);

  // 2. 输入框状态
  const [inputValue, setInputValue] = useState("");

  // 3. 当前筛选条件:'all' | 'done' | 'undone'
  const [filter, setFilter] = useState("all");

  // 获取下一个 id(简单做法:最大 id + 1)
  const [nextId, setNextId] = useState(3);

  // ====== 增:添加新任务 ======
  const addTodo = () => {
    const text = inputValue.trim();
    if (text === "") return; // 空内容不添加

    // 创建新任务对象,追加到数组末尾
    setTodos([...todos, { id: nextId, text, done: false }]);
    setNextId(nextId + 1); // id 递增
    setInputValue(""); // 清空输入框
  };

  // ====== 删:根据 id 删除任务 ======
  const deleteTodo = (id) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  // ====== 改:切换完成状态 ======
  const toggleTodo = (id) => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo,
      ),
    );
  };

  // ====== 改:编辑任务文本 ======
  const updateText = (id, newText) => {
    setTodos(
      todos.map((todo) => (todo.id === id ? { ...todo, text: newText } : todo)),
    );
  };

  // ====== 查:根据筛选条件过滤展示 ======
  const filteredTodos = todos.filter((todo) => {
    if (filter === "done") return todo.done;
    if (filter === "undone") return !todo.done;
    return true; // 'all'
  });

  // ====== 统计数据(派生值,不需要 useState)======
  const totalCount = todos.length;
  const doneCount = todos.filter((t) => t.done).length;

  // 处理键盘事件:按 Enter 添加
  const handleKeyDown = (e) => {
    if (e.key === "Enter") addTodo();
  };

  return (
    <div style={{ maxWidth: 500, margin: "0 auto" }}>
      <h2>Todo 列表</h2>

      {/* ====== 输入区域 ====== */}
      <div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder="输入新任务,按 Enter 添加"
          style={{ flex: 1, padding: 8 }}
        />
        <button onClick={addTodo}>添加</button>
      </div>

      {/* ====== 筛选按钮 ====== */}
      <div style={{ marginBottom: 16 }}>
        {[
          { key: "all", label: "全部" },
          { key: "undone", label: "未完成" },
          { key: "done", label: "已完成" },
        ].map(({ key, label }) => (
          <button
            key={key}
            onClick={() => setFilter(key)}
            style={{
              marginRight: 8,
              fontWeight: filter === key ? "bold" : "normal",
              color: filter === key ? "#1976d2" : "#666",
            }}
          >
            {label}
          </button>
        ))}
      </div>

      {/* ====== 任务列表 ====== */}
      {filteredTodos.length === 0 ? (
        <p style={{ color: "#999" }}>暂无任务</p>
      ) : (
        <ul style={{ listStyle: "none", padding: 0 }}>
          {filteredTodos.map((todo) => (
            <li
              key={todo.id}
              style={{
                display: "flex",
                alignItems: "center",
                padding: "8px 0",
                borderBottom: "1px solid #eee",
              }}
            >
              {/* 复选框:切换完成状态 */}
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => toggleTodo(todo.id)}
                style={{ marginRight: 8 }}
              />

              {/* 任务文本 */}
              <span
                style={{
                  flex: 1,
                  textDecoration: todo.done ? "line-through" : "none",
                  color: todo.done ? "#999" : "#333",
                }}
              >
                {todo.text}
              </span>

              {/* 编辑按钮 */}
              <button
                onClick={() => {
                  const newText = prompt("修改任务", todo.text);
                  if (newText && newText.trim()) {
                    updateText(todo.id, newText.trim());
                  }
                }}
                style={{ marginRight: 8 }}
              >
                编辑
              </button>

              {/* 删除按钮 */}
              <button onClick={() => deleteTodo(todo.id)}>删除</button>
            </li>
          ))}
        </ul>
      )}

      {/* ====== 统计信息 ====== */}
      <div style={{ marginTop: 16, color: "#666", fontSize: 14 }}>
        共 {totalCount} 项,已完成 {doneCount} 项
      </div>
    </div>
  );
}

教学要点回顾:这个案例综合运用了数组不可变更新(展开运算符、filter、map)、多状态管理、派生值计算、以及条件渲染,是一次完整的 useState 能力检验。


5.2 案例二:表单多字段管理

实现一个注册表单,包含用户名、密码、手机号,附带实时校验提示。

import { useState } from "react";

function RegisterForm() {
  // 表单数据用对象管理(字段属于同一实体)
  const [form, setForm] = useState({
    username: "",
    password: "",
    phone: "",
  });

  // 错误信息也用一个对象管理
  const [errors, setErrors] = useState({});

  // 通用字段更新函数
  const handleChange = (field, value) => {
    setForm((prev) => ({ ...prev, [field]: value }));

    // 更新时实时清除该字段的错误提示
    if (errors[field]) {
      setErrors((prev) => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  };

  // 表单校验
  const validate = () => {
    const newErrors = {};

    if (form.username.trim().length < 3) {
      newErrors.username = "用户名至少 3 个字符";
    }

    if (form.password.length < 6) {
      newErrors.password = "密码至少 6 位";
    }

    if (!/^1[3-9]\d{9}$/.test(form.phone)) {
      newErrors.phone = "请输入有效的手机号";
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0; // 无错误则通过
  };

  // 提交
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止默认表单提交行为
    if (validate()) {
      alert("注册成功!\n" + JSON.stringify(form, null, 2));
    }
  };

  // 重置
  const handleReset = () => {
    setForm({ username: "", password: "", phone: "" });
    setErrors({});
  };

  return (
    <div style={{ maxWidth: 400, margin: "0 auto" }}>
      <h2>注册表单</h2>

      <form onSubmit={handleSubmit}>
        {/* 用户名 */}
        <div style={{ marginBottom: 16 }}>
          <label>用户名:</label>
          <input
            value={form.username}
            onChange={(e) => handleChange("username", e.target.value)}
            placeholder="至少 3 个字符"
            style={{
              display: "block",
              width: "100%",
              padding: 8,
              marginTop: 4,
              border: errors.username ? "1px solid red" : "1px solid #ccc",
            }}
          />
          {errors.username && (
            <span style={{ color: "red", fontSize: 12 }}>
              {errors.username}
            </span>
          )}
        </div>

        {/* 密码 */}
        <div style={{ marginBottom: 16 }}>
          <label>密码:</label>
          <input
            type="password"
            value={form.password}
            onChange={(e) => handleChange("password", e.target.value)}
            placeholder="至少 6 位"
            style={{
              display: "block",
              width: "100%",
              padding: 8,
              marginTop: 4,
              border: errors.password ? "1px solid red" : "1px solid #ccc",
            }}
          />
          {errors.password && (
            <span style={{ color: "red", fontSize: 12 }}>
              {errors.password}
            </span>
          )}
        </div>

        {/* 手机号 */}
        <div style={{ marginBottom: 16 }}>
          <label>手机号:</label>
          <input
            value={form.phone}
            onChange={(e) => handleChange("phone", e.target.value)}
            placeholder="11 位手机号"
            style={{
              display: "block",
              width: "100%",
              padding: 8,
              marginTop: 4,
              border: errors.phone ? "1px solid red" : "1px solid #ccc",
            }}
          />
          {errors.phone && (
            <span style={{ color: "red", fontSize: 12 }}>{errors.phone}</span>
          )}
        </div>

        {/* 按钮 */}
        <div style={{ display: "flex", gap: 12 }}>
          <button type="submit">注册</button>
          <button type="button" onClick={handleReset}>
            重置
          </button>
        </div>
      </form>
    </div>
  );
}

5.3 与 useEffect 的基础配合

useState 负责"记住数据",useEffect 负责"在数据变化时执行副作用"。下面是两个基础配合场景。

场景一:状态变化时同步到 localStorage

import { useState, useEffect } from "react";

function ThemeSwitcher() {
  // 从 localStorage 读取初始主题(懒初始化)
  const [theme, setTheme] = useState(() => {
    return localStorage.getItem("theme") || "light";
  });

  // 当 theme 变化时,自动同步到 localStorage
  useEffect(() => {
    localStorage.setItem("theme", theme);
    console.log(`主题已切换为:${theme}`);
  }, [theme]); // 依赖 theme,theme 变就执行

  const toggleTheme = () => {
    setTheme((prev) => (prev === "light" ? "dark" : "light"));
  };

  return (
    <div
      style={{
        backgroundColor: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#333" : "#fff",
        padding: 20,
        borderRadius: 8,
        transition: "all 0.3s",
      }}
    >
      <p>当前主题:{theme}</p>
      <button onClick={toggleTheme}>切换主题</button>
    </div>
  );
}

场景二:搜索防抖(state + effect 经典组合)

import { useState, useEffect } from "react";

function SearchBox() {
  const [keyword, setKeyword] = useState("");
  const [debouncedKeyword, setDebouncedKeyword] = useState("");

  // keyword 变化后等待 500ms 再更新 debouncedKeyword
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedKeyword(keyword);
    }, 500);

    // 清理函数:下次 effect 执行前清除上一次的定时器
    return () => clearTimeout(timer);
  }, [keyword]);

  // debouncedKeyword 变化时发起搜索
  useEffect(() => {
    if (debouncedKeyword) {
      console.log("发起搜索:", debouncedKeyword);
      // 实际项目中这里调用 API
    }
  }, [debouncedKeyword]);

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="输入关键词搜索"
      />
      <p>实际搜索词:{debouncedKeyword}</p>
    </div>
  );
}

数据流图解:

用户输入 "react"
  → keyword: "" → "r" → "re" → "rea" → "reac" → "react"
  → 每次变化都触发 useEffect(设定 500ms 计时器)
  → 但只有最后一次的计时器不会被清除
  → 500ms 后 debouncedKeyword 变为 "react"
  → 触发搜索 effect → 发起 API 请求

这样避免用户每敲一个字母就发一次请求,只在输入停顿后发一次。

第六部分:自测练习题

练习题一:基础填空

补全以下代码,实现一个点击按钮数字翻倍的计数器。

import { useState } from "react";

function DoubleCounter() {
  // TODO: 声明一个 count 状态,初始值为 1

  const handleDouble = () => {
    // TODO: 使 count 翻倍
  };

  return (
    <div>
      <p>当前值:{/* TODO: 显示 count */}</p>
      <button onClick={handleDouble}>翻倍</button>
    </div>
  );
}
点击查看答案
import { useState } from "react";

function DoubleCounter() {
  const [count, setCount] = useState(1);

  const handleDouble = () => {
    setCount(count * 2);
    // 更安全的写法(依赖旧值时推荐):
    // setCount(prev => prev * 2);
  };

  return (
    <div>
      <p>当前值:{count}</p>
      <button onClick={handleDouble}>翻倍</button>
    </div>
  );
}

练习题二:判断对错

以下代码是否能正常让 count 的值 +2?如果不能,请指出问题并修正。

function BadCounter() {
  const [count, setCount] = useState(0);

  const addTwo = () => {
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={addTwo}>+2</button>
    </div>
  );
}
点击查看答案

不能。点击后 count 只会 +1,不会 +2。

原因:两次 setCount(count + 1) 中的 count 都是同一个快照值(0),等价于两次 setCount(1)。由于批量更新,最终结果就是 1。

修正方案:使用更新函数形式。

const addTwo = () => {
  setCount((prev) => prev + 1);
  setCount((prev) => prev + 1);
};

练习题三:数组操作

编写代码实现:点击按钮向列表中添加一个随机数(1-100),并显示列表中的所有数字,用逗号分隔。

点击查看答案
import { useState } from "react";

function RandomList() {
  const [numbers, setNumbers] = useState([]);

  const addRandom = () => {
    const random = Math.floor(Math.random() * 100) + 1;
    setNumbers([...numbers, random]);
    // 或:setNumbers(prev => [...prev, random]);
  };

  return (
    <div>
      <button onClick={addRandom}>添加随机数</button>
      <p>列表:{numbers.join(", ") || "暂无数据"}</p>
    </div>
  );
}

练习题四:对象更新

以下代码中,点击按钮后页面显示的年龄会变化吗?如果不会,为什么?请修正。

function UserCard() {
  const [user, setUser] = useState({ name: "小明", age: 18 });

  const growUp = () => {
    user.age = user.age + 1;
    setUser(user);
  };

  return (
    <div>
      <p>
        {user.name},{user.age} 岁
      </p>
      <button onClick={growUp}>长大一岁</button>
    </div>
  );
}
点击查看答案

不会变化。

原因user.age = user.age + 1 直接修改了原对象,然后 setUser(user) 传入的仍是同一个引用。React 用 Object.is() 比较,发现引用没变,认为状态没有变化,不会重渲染。

修正方案:

const growUp = () => {
  setUser({ ...user, age: user.age + 1 });
  // 或使用更新函数:
  // setUser(prev => ({ ...prev, age: prev.age + 1 }));
};

练习题五:综合实战

实现一个"购物车"组件,要求:

  1. 初始时有三个商品,每个商品包含 id, name, price, quantity
  2. 每个商品旁边有 + 和 - 按钮可以增减数量(数量不能小于 0,减到 0 时自动删除该商品)
  3. 底部显示总数量和总价(派生值,不用 useState)
点击查看答案
import { useState } from "react";

function ShoppingCart() {
  const [items, setItems] = useState([
    { id: 1, name: "React 入门教程", price: 59, quantity: 1 },
    { id: 2, name: "TypeScript 实战", price: 79, quantity: 2 },
    { id: 3, name: "VSCode 键帽", price: 29, quantity: 1 },
  ]);

  // 增加数量
  const increase = (id) => {
    setItems((prev) =>
      prev.map((item) =>
        item.id === id ? { ...item, quantity: item.quantity + 1 } : item,
      ),
    );
  };

  // 减少数量
  const decrease = (id) => {
    setItems(
      (prev) =>
        prev
          .map((item) =>
            item.id === id ? { ...item, quantity: item.quantity - 1 } : item,
          )
          .filter((item) => item.quantity > 0), // 数量为 0 则移除
    );
  };

  // 派生值:不需要 useState
  const totalQuantity = items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0,
  );

  return (
    <div>
      <h2>购物车</h2>
      {items.length === 0 ? (
        <p>购物车是空的</p>
      ) : (
        <ul>
          {items.map((item) => (
            <li key={item.id} style={{ marginBottom: 8 }}>
              <span>{item.name}</span>
              <span style={{ margin: "0 12px", color: "#666" }}>
                ¥{item.price} × {item.quantity}
              </span>
              <button onClick={() => decrease(item.id)}>-</button>
              <span style={{ margin: "0 8px" }}>{item.quantity}</span>
              <button onClick={() => increase(item.id)}>+</button>
            </li>
          ))}
        </ul>
      )}
      <hr />
      <p>
        总计:{totalQuantity} 件商品,¥{totalPrice}
      </p>
    </div>
  );
}

全文总结:useState 是 React 函数组件的基石——它让函数组件拥有了"记忆"。掌握好 useState,你就能在 React 中自由地管理一切本地状态。记住三大铁律:顶层调用、不可变更新、依赖旧值时用更新函数。把这三个原则刻在脑子里,你就避开了 90% 的新手坑。


0
博主关闭了当前页面的评论