React面试题:Hooks 为什么不能在分支和循环里面写?

因为 React 靠 Hook 的调用顺序来匹配状态,顺序一变,状态就错位了。

这句话就是核心,不要一上来就讲 Fiber、链表、源码。面试官继续追问,再往下展开。


面试官继续追问后的展开

1. React 到底怎么"找状态"?

函数组件对应一个 Fiber,Fiber 上会挂着这个组件的 Hook 链表:

text 复制代码
Fiber
  ↓
Hook1 → Hook2 → Hook3 → null

比如:

js 复制代码
function App() {
  const [name] = useState('张三');
  const [age] = useState(18);

  useEffect(() => {}, []);
}

React 实际上会形成:

text 复制代码
Hook1 → useState(name)
   ↓
Hook2 → useState(age)
   ↓
Hook3 → useEffect

下一次 render 时,React 还是按照 Hook 的调用顺序去匹配:

text 复制代码
第 1 次 Hook 调用 → Hook1
第 2 次 Hook 调用 → Hook2
第 3 次 Hook 调用 → Hook3

所以 React 并不是靠:

js 复制代码
name
age

这些变量名找状态,而是靠Hook 的位置和顺序。


2. 为什么 if 会出问题?

例如:

js 复制代码
function App({ isLogin }) {
  if (isLogin) {
    useState('张三');
  }

  useState(18);
}

第一次:

text 复制代码
isLogin = true

Hook1 → 张三
Hook2 → 18

下一次:

text 复制代码
isLogin = false

Hook1 → 18

问题就在这里:

text 复制代码
第一次:
第1个 Hook → name
第2个 Hook → age

第二次:
第1个 Hook → age

age 本来是第二个 Hook,现在突然变成第一个 Hook 了。

于是 React 按位置拿到的状态就错位了。


3. 所以真正禁止的是什么?

不是简单的:

"Hooks 不能写在 if 里。"

而是:

每次 render,Hooks 的调用顺序必须稳定。

所以这些写法都可能破坏这个规则:

js 复制代码
if (condition) {
  useState();
}
js 复制代码
for (...) {
  useEffect();
}
js 复制代码
while (...) {
  useMemo();
}

甚至:

js 复制代码
function foo() {
  useState();
}

然后:

js 复制代码
if (condition) {
  foo();
}

本质都是一个问题:

text 复制代码
Hook 调用顺序可能变化
        ↓
Hook 对不上
        ↓
状态 / effect / memo 等数据错位

4. 为什么循环也不能写?

例如:

js 复制代码
for (let i = 0; i < count; i++) {
  useState(i);
}

第一次:

text 复制代码
count = 3

Hook1
Hook2
Hook3

下一次:

text 复制代码
count = 2

Hook1
Hook2

Hook 的调用数量变了,后面的 Hook 位置自然就变了。

这里有个容易说错的地方

如果你写:

js 复制代码
for (let i = 0; i < 3; i++) {
  useState(i);
}

而且每次 render 都永远执行 3 次,单纯从"顺序是否稳定"这个底层条件来说,确实不一定产生错位。所以面试时不要说:

"循环一定会导致 Hook 顺序错乱。"

更准确的是:

循环的迭代次数是动态的,很容易改变 Hook 的调用次数;React 因此统一要求 Hook 只能在顶层调用。


5. 为什么自定义 Hook 也不能放进分支?

例如:

js 复制代码
function useUser() {
  const [name] = useState('张三');
  return name;
}

function App({ isLogin }) {
  if (isLogin) {
    useUser();
  }

  const [age] = useState(18);
}

很多人以为:

text 复制代码
App Fiber
 ├── useUser
 └── useState

其实不是。自定义 Hook 本身只是普通函数。

执行:

js 复制代码
useUser();

实际上就是执行里面的:

js 复制代码
useState();

所以最终还是:

text 复制代码
App Fiber
   ↓
Hook1 → useUser 里的 useState
Hook2 → age

如果 isLogin 改变:

text 复制代码
第一次:
Hook1 → name
Hook2 → age

第二次:
Hook1 → age

一样会错位。所以:

自定义 Hook 本身不占一个 Hook 位置,但它里面调用的 Hook 会占位置。


6. 自定义 Hook 的状态到底属于谁?

这是继续往深处追时很重要的一点。

属于调用这个自定义 Hook 的组件 Fiber。

js 复制代码
function useCounter() {
  const [count, setCount] = useState(0);
  return [count, setCount];
}

如果:

js 复制代码
function A() {
  useCounter();
}

function B() {
  useCounter();
}

并不是:

text 复制代码
useCounter
   ↓
自己的 state

而是:

text 复制代码
A Fiber
 └── Hook → count


B Fiber
 └── Hook → count

因为自定义 Hook 没有自己的 Fiber,也没有独立的 Hook 链表。

它只是:

把一组 Hook 调用封装起来。


7. 为什么 useEffect 里面不能再调用自定义 Hook?

例如:

js 复制代码
useEffect(() => {
  useUser(); // ❌
}, []);

因为 useEffect 的 callback 不是组件 render 的一部分。

真正的时间顺序是:

text 复制代码
组件 render
   ↓
调用 useEffect()
   ↓
注册 effect
   ↓
render 结束
   ↓
之后执行 effect callback
   ↓
useUser()
   ↓
useState()

此时已经不在 React 当前这次 render 的 Hook 调用过程中。所以不是:

"自定义 Hook 不能在 useEffect 里用。"

而是:

React Hook 必须在组件或自定义 Hook 的 render 调用链里执行,不能等到 effect、事件回调等普通函数执行时再调用。


8. 为什么 React 要采用这种设计?

因为 Hook API 没有要求你显式传:

js 复制代码
useState('name', '某个唯一ID')

React 需要知道:

text 复制代码
这次 useState
到底对应上一次哪个 useState?

它采用的办法非常简单:

text 复制代码
当前组件 Fiber
      ↓
Hook 链表
      ↓
第 1 个调用 → 第 1 个 Hook
第 2 个调用 → 第 2 个 Hook
第 3 个调用 → 第 3 个 Hook

所以整个规则的第一性原理就是:

text 复制代码
Hook 顺序稳定
      ↓
位置稳定
      ↓
状态才能正确对应

反过来:

text 复制代码
Hook 顺序变化
      ↓
位置变化
      ↓
状态错位

9. 最后用一个完整例子串起来

js 复制代码
function useUser() {
  const [name] = useState('张三');
  return name;
}

function App({ isLogin }) {
  const name = useUser();

  const [age] = useState(18);

  useEffect(() => {
    console.log(name);
  }, [name]);

  return ...
}

React 看到的 Hook 顺序其实就是:

text 复制代码
App Fiber
   ↓
Hook1 → useUser 里的 useState
Hook2 → App 里的 useState
Hook3 → App 里的 useEffect

下一次 render 必须还是:

text 复制代码
Hook1 → useUser 里的 useState
Hook2 → App 里的 useState
Hook3 → App 里的 useEffect

如果你写成:

js 复制代码
if (isLogin) {
  useUser();
}

那么:

text 复制代码
登录:

Hook1 → useUser
Hook2 → age
Hook3 → effect


未登录:

Hook1 → age
Hook2 → effect

从这里开始,整个 Hook 链就错位了。


最终满分回答

Hooks 不能写在分支、循环里,根本原因不是语法限制,而是 React 按调用顺序匹配 Hook。每个组件 Fiber 上都有一条 Hook 链表,每次 render 都按顺序取对应的 Hook;一旦分支或循环改变了 Hook 的调用次数或顺序,前后的 Hook 就对不上,状态、effect、memo 等数据就会错位。

这道题建议你最终记住三个层次

text 复制代码
第一层:为什么不能写?
→ React 按调用顺序匹配 Hook。

第二层:顺序乱了会怎样?
→ Hook 对不上,状态等数据错位。

第三层:底层怎么实现?
→ Fiber.memoizedState 指向 Hook 链表,
  render 时按顺序遍历和匹配。

第一句只说第一层。后面两层,等面试官追问再展开。

相关推荐
FungLeo1 小时前
成为全栈·Next.js 网站前台篇·响应式、可访问性与错误状态:内容站不能只验桌面首页
react.js·响应式设计·next.js·移动端适配·成为全栈·web 可访问性
开开心心就好1 小时前
视频压缩工具推荐,绿色版免安装双击即用
java·前端·人工智能·spring·智能手机·intellij-idea·excel
西红柿炖牛腩3541 小时前
GLB 转 OBJ/PLY 后体积没变?3D 模型不减面变小靠什么
前端·javascript·3d
欣欣之王来了1 小时前
前端开发学习路线图:2026年Vue方向最新版
前端·学习·架构·项目·vue3教程
夏幻灵1 小时前
前端八股:Vue2 与 Vue3 的核心区别:响应式、Composition API 与性能优化
前端
ss2732 小时前
AI全栈实战 | 3.5-01 Python 全栈前端最小集:不会 React 也能做出能看的后台,关键就一条线
前端·python·react.js
人工智能培训2 小时前
数字孪生驱动大模型工业知识库:为具身机器人植入领域专业经验
大数据·linux·服务器·前端·人工智能
维克兜率天2 小时前
【维克】通道突破:画出趋势的“轨道线“
前端·数据库·人工智能