React Hooks深入

写给初学者:每个概念都从”为什么需要它”开始讲,配合通俗比喻和完整可运行代码。


1.1 为什么「连点三次 +1」可能只加了 1?

function Counter() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count + 1);   // ❌ 三行都等价于 setCount(0 + 1)
    setCount(count + 1);   // 因为 handleClick 执行那瞬间 count 还是 0
    setCount(count + 1);
  }
  // 结果:count = 1,不是 3
}

通俗比喻:你让三个人分别”把仓库里的苹果数量 +1 并存回去”。但三个人看的是同一张老清单(写着 0),都以为现在有 0 个,都存了 1 回去——结果仓库还是 1 个。

1.2 函数式更新

setCount 传一个函数,React 会用”它内部最真实的最新值”来调用:

function Counter() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(prev => prev + 1);   // ✅ prev 是 React 给的最新值
    setCount(prev => prev + 1);   // ✅ 这次的 prev 已经是 1
    setCount(prev => prev + 1);   // ✅ 这次的 prev 已经是 2
  }
  // 点击一次 → count: 0 → 3 ✅
}

什么时候用函数式更新

  • 新值无关旧值:setCount(0)setName(input)
  • 新值基于旧值:setCount(prev => prev + 1)

1.3 对象状态——必须整个换掉

// ❌ 直接改属性——React 不会重新渲染!
user.age = user.age + 1;
 
// ✅ 创建一个新对象
setUser({ ...user, age: user.age + 1 });
 
// 函数式更新版本(更稳)
setUser(prev => ({ ...prev, age: prev.age + 1 }));
// 注意:箭头函数返回对象要加括号 () => ({ ... })

通俗比喻:React 是个只认收据的仓库管理员。你必须交一张新的入库单{...user, age: 26}),管理员才肯重新盘点。

1.4 数组状态——同样要换新数组

想做的事写法Python 对照
末尾加一个[...arr, item]arr + [item]
开头加一个[item, ...arr][item] + arr
删掉下标 i 的arr.filter((_, idx) => idx !== i)列表推导
合并两数组[...a, ...b]a + b

1.5 初值的惰性初始化

// ⚠️ 每次渲染都会执行 computeInitial(),浪费
const [data, setData] = useState(computeInitial());
 
// ✅ 传函数本身,React 只在第一次渲染时调用
const [data, setData] = useState(() => computeInitial());
 
// 实际场景——从 localStorage 读初始值
const [name, setName] = useState(() => {
  return localStorage.getItem("myName") || "匿名";
});

一、useState 的三条铁律

铁律 1:只在组件顶层调用,不能放进 if/循环/函数里

function Good({ isVip }) {
  // ✅ 所有 useState 都在最顶层
  const [a, setA] = useState(0);
  const [b, setB] = useState(isVip ? 1 : 0);
}

React 内部用数组按顺序存每个 useState 的值,靠”调用顺序”对应。放进 if 里会导致错位。

通俗比喻:像在食堂打饭按顺序领餐盘。你今天插队少领一个,后面所有人拿到的餐盘就全是别人的。

铁律 2:不可变更新——永远创建新值,不直接改老值

// ❌ 直接改
user.age = 26; setUser(user);
todos.push("x"); setTodos(todos);
 
// ✅ 生成新值
setUser({ ...user, age: 26 });
setTodos([...todos, "x"]);

铁律 3:拆分状态,别把所有东西塞一个对象

// ❌ 把无关的状态塞一坨
const [form, setForm] = useState({ name: "", age: 0, loading: false, error: null });
 
// ✅ 无关的状态分开
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [loading, setLoading] = useState(false);
 
// 例外:总是一起变化的放一起
const [pos, setPos] = useState({ x: 0, y: 0 });

二、受控组件

受控组件 = 输入框的值由 React 状态控制,用户输入立刻同步到状态。

// input
function NameInput() {
  const [name, setName] = useState("");
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <p>你好, {name}!</p>
    </div>
  );
}
 
// checkbox(注意是 checked 不是 value)
<input
  type="checkbox"
  checked={agreed}
  onChange={e => setAgreed(e.target.checked)}
/>
 
// select
<select value={color} onChange={e => setColor(e.target.value)}>
  <option value="red">红色</option>
  <option value="blue">蓝色</option>
</select>
 
// textarea(React 统一用 value 属性)
<textarea value={bio} onChange={e => setBio(e.target.value)} />

受控组件统一定律:不管哪种表单元素,都是「一个状态 × value/checked 属性 × onChange 同步」。

表单提交时务必 e.preventDefault() 阻止页面刷新。


1.6 基本结构

useEffect(() => {
  // 副作用代码(渲染"完"之后执行)
 
  return () => {
    // 清理函数(组件消失时、或下次 effect 执行前调用)
  };
}, [依赖1, 依赖2]);

三要素:

  • 副作用函数:干正事(发请求、设定时器)
  • 清理函数:收拾烂摊子(清定时器、取消订阅)
  • 依赖数组:告诉 React”什么变了要重新执行”

1.7 四种使用模式

模式写法用途
只跑一次useEffect(fn, [])初始加载、设全局定时器
依赖变化才跑useEffect(fn, [id])依赖某些值的副作用
每次渲染都跑useEffect(fn)几乎不用
带清理return () => clearInterval(t)定时器/订阅

1.8 完整的异步数据获取模式(实战必背)

function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    async function load() {
      try {
        setLoading(true);
        const res = await fetch("/api/users");
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data: User[] = await res.json();
        setUsers(data);
      } catch (e) {
        setError(e instanceof Error ? e.message : "未知错误");
      } finally {
        setLoading(false);
      }
    }
    load();
  }, []);
 
  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

这个”loading / error / data 三态 + try/catch/finally + 包 async 函数”模式,是 React 里最常见的数据获取写法。


速查表

概念一句话关键代码
函数式更新基于”最新值”计算新值setCount(prev => prev + 1)
对象状态必须整个换新对象setUser({...prev, age: 26})
受控 inputvalue 受状态控制<input value={t} onChange={...} />
useEffect副作用(请求/定时器/订阅)useEffect(fn, deps)
数据获取三态loading/error/datauseState(true)/useState<null>(null)

速记卡(面试闪卡)

Q1:一句话讲清「React Hooks深入」到底是什么? A:React Hooks 让函数组件也能管状态跑副作用:useState 记数据、useEffect 管请求/定时器,但都靠”调用顺序”和”不可变更新”两条铁律。

Q2:useState 为什么连点三次只加 1 —— 怎么理解? A:像三个人看同一张写着 0 的老清单,都以为现在 0 个、各存回 1——仓库还是 1。根因:三个 setCount(count+1) 用的都是点击瞬间那个旧 count。解法传函数 setCount(prev => prev+1),React 会给”它内部最新的真实值”,三次就真加到 3。这就是函数式更新。

Q3:useState 的三条铁律 —— 怎么理解? A:① 只在顶层调用——React 用数组按”顺序”存每个 Hook 的值,塞进 if 里会错位(像食堂打饭插队少领一个,后面全拿错餐盘);② 不可变更新——永远建新值(setUser({...user, age:26})),直接改属性 React 不重渲染;③ 拆分状态,无关的别塞一坨对象。

Q4:useEffect 怎么用 —— 怎么理解? A:useEffect 像”渲染完后才开工的清洁工”:副作用函数干正事(发请求、设定时器),return 的清理函数收烂摊子(清定时器、取消订阅),依赖数组告诉它”什么变了才重跑”。四种模式:只跑一次 []、依赖变才跑 [id]、每次都跑(几乎不用)、带清理。异步数据获取必背 loading/error/data 三态。

Q5:受控组件是什么 —— 怎么理解? A:受控组件 = 输入框的值由 React 状态说了算,用户输入立刻同步回状态。统一公式就一句:一个状态 × value/checked 属性 × onChange 同步——input 用 value、checkbox 用 checked、select/textarea 同理。提交时记得 e.preventDefault() 拦掉页面刷新。

Q6:核心速记主线有哪些?

  • useState:函数式更新用 prev,对象/数组要换新
  • 三条铁律:顶层调用、不可变、拆状态
  • useEffect:副作用+清理+依赖数组,异步三态
  • 受控组件:状态控 value,onChange 同步

口诀 A:Hooks 函数组件管状态,顺序存值别乱排 连点三次只加一,函数更新 prev 来 不可变更新建新值,顶层调用莫塞 if useEffect 收烂摊,受控一态一同步

相关链接