React 一面:Hooks 常见考点(useState/useEffect 与自定义 Hook)
现在很多前端一面都会问到 Hooks,尤其是 useState/useEffect 的规则和坑点,这一篇挑几个常见考点和一两道面试题做个系统整理。
Hooks 的基本规则(常见概念题)
React 官方提出了两条“Hooks 规则”:
- 只在最顶层调用 Hook(不要在循环、条件判断或嵌套函数中调用);
- 只在 React 函数组件或自定义 Hook 中调用 Hook。
一面常见问法:
- “为什么不能在条件里调用 Hook?”
- “React 是怎么知道每次 useState/useEffect 对应的是哪个状态的?”
参考回答要点:
- React 依靠“调用顺序”来匹配每一次渲染中的 Hook 状态,如果在条件/循环中调用,会打乱顺序;
- 顶层调用可以保证每次渲染时 Hook 的调用顺序是一致的,从而正确地读写对应的状态/副作用。
useState:异步更新与批量更新
useState 常见考点:
- 状态更新的异步性:连续多次
setCount(count + 1)不一定会得到预期结果; - 函数式更新:
setCount((prev) => prev + 1)的优点; - 批量更新:在同一个事件处理函数中,多次状态更新会被批量合并。
面试时可以用一个简单例子:
1const [count, setCount] = useState(0);
2
3function handleClick() {
4 setCount(count + 1);
5 setCount(count + 1);
6}
解释:
- 上面的写法在一次点击中最终只会 +1;
- 因为两次读取到的
count都是旧值 0,React 会批量合并更新; - 如果想在一次点击中递增 2,应该用函数式更新:
1function handleClick() {
2 setCount((prev) => prev + 1);
3 setCount((prev) => prev + 1);
4}
useEffect:依赖数组、清理函数与常见 bug
useEffect 常见考点:
- 依赖数组的含义:不传 / 空数组 / 含依赖 的区别;
- 清理函数(
return () => {})在组件卸载或依赖变化时的执行时机; - 忘记补依赖导致的“读取旧值”或“无限循环请求”问题。
举个典型请求场景的例子:
1useEffect(() => {
2 let canceled = false;
3 async function fetchData() {
4 const res = await fetch("/api/data?id=" + id);
5 const json = await res.json();
6 if (!canceled) {
7 setData(json);
8 }
9 }
10 fetchData();
11 return () => {
12 canceled = true;
13 };
14}, [id]);
解释要点:
- 依赖数组
[id]表示id变化时重新请求; - 清理函数用来在组件卸载或 id 改变时标记“本次请求结果不再需要”,避免竞态条件。
自定义 Hook:复用逻辑而不是复用 JSX
一面有时会问:“自定义 Hook 有什么用?能不能举个例子?”
可以简要说明:
- 自定义 Hook 的目的,是复用状态逻辑(如数据获取、订阅、表单处理),而不是复用 UI 本身;
- 它本质上就是一个函数,内部可以调用其它 Hook,并返回一组状态/方法。
简单例子:
1function useWindowSize() {
2 const [size, setSize] = useState({
3 width: window.innerWidth,
4 height: window.innerHeight
5 });
6
7 useEffect(() => {
8 function onResize() {
9 setSize({
10 width: window.innerWidth,
11 height: window.innerHeight
12 });
13 }
14 window.addEventListener("resize", onResize);
15 return () => window.removeEventListener("resize", onResize);
16 }, []);
17
18 return size;
19}
组件中使用:
1function MyComponent() {
2 const { width, height } = useWindowSize();
3 return <div>{width} x {height}</div>;
4}
常见面试题与参考答案
题 1:为什么不能在条件语句中调用 Hook?
参考答案要点:
- React 依靠“函数组件中 Hook 的调用顺序”来管理状态;
- 如果在条件或循环中调用 Hook,不同渲染之间调用顺序可能不一致,React 无法正确匹配每个 useState/useEffect 对应的内部状态槽;
- 因此要求 Hook 只能在组件顶层调用,保证每次渲染顺序一致。
可以补一句:
“这也是为什么 React 官方提供了 eslint-plugin-react-hooks 来静态检查 Hook 调用位置。”
题 2:多次调用 setState 会发生什么?如何正确累加?
可以用前面的 setCount 示例来回答:
- 在同一个事件处理函数中,多次直接用
setCount(count + 1),最终只会效果等同于一次增加 1; - 正确累加应该使用函数式更新形式
setCount(prev => prev + 1),这样每次更新都基于上一次的结果。
如果面试官追问“为什么会这样”,可以说:
- React 会在事件处理过程中批量合并状态更新,直接使用
count + 1其实多次读到的是同一个旧值; - 函数式更新则能确保每个更新都基于最新的 state。
题 3:useEffect 依赖数组空/不空各有什么含义?如何避免常见 bug?
参考答案要点:
- 不传依赖数组:每次渲染后都会执行 effect;
- 传空数组
[]:只在组件挂载时执行一次,卸载时执行清理函数; - 传
[dep]:在挂载时执行一次,在dep变化时再次执行(且先执行上一次的清理函数)。
避免 bug 的关键是:
- 不要随意漏掉依赖,否则 effect 可能读取到“旧值”;
- 对于请求类 effect,要注意在卸载/参数变化时取消或标记结果无效,避免竞态条件。
如果能结合一个“忘记加依赖导致无限请求”的例子来讲,会更有说服力。