前言
在React中,Hooks是非常重要的内容,很多人在学习或者备考的时候遇到各种各样的Hooks函数难免会头晕,太多了容易记混,这篇文章通过问答的格式来帮你巩固一下Hooks相关的考点,看看你能不能接住这些大厂中的常考问题吧!
useState
useState是最基础的Hook,用来得到 响应式状态 和 更新响应式状态的函数,使用方式为:
js
const [state,setState] = useState("initival")
返回一个响应式状态state,其初始值为initval,还返回了更新状态的函数setState。下面就是useState相关的一些Q&A了
Q1: 请解释useState的基本用法,并写一个简单的计数器示例
答案:
useState是React提供的一个Hook,它允许我们在函数组件中添加和管理状态。基本用法如下:
jsx
import React, { useState } from 'react';
function Counter() {
// 声明一个叫count的状态变量,初始值为0
// setCount是用来更新count的函数
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数: {count}</p>
{/* 点击按钮时调用setCount更新状态 */}
<button onClick={() => setCount(count + 1)}>
点击增加
</button>
</div>
);
}
这道题很基础了,用来小试牛刀,找找感觉
Q2:下面这段代码有什么问题?如何改进?
jsx
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<p>计数: {count}</p>
<button onClick={handleClick}>
快速增加2
</button>
</div>
);
}
答案:
点击按钮时,计数只会增加1而不是预期的2。这是因为React会批量处理状态更新,两次setCount都基于相同的count值。
改进方案 :
使用函数式更新,确保基于前一个状态值进行更新:
jsx
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>计数: {count}</p>
<button onClick={handleClick}>
快速增加2
</button>
</div>
);
}
如果对这道题不熟悉的同学可以看看我之前写的文章:理解React中的状态批处理与更新队列,详细介绍了批处理机制!
Q3: 当我们需要在useState中初始化一个复杂对象时,哪种方式更合适?为什么?
题目代码:
jsx
// 方式一:直接初始化
const [user, setUser] = useState({
name: '张三',
age: 25,
preferences: {
theme: 'dark',
fontSize: 16
}
});
// 方式二:通过函数初始化
const [user, setUser] = useState(() => ({
name: '张三',
age: 25,
preferences: {
theme: 'dark',
fontSize: 16
}
}));
答案
更合适的方式:方式二(通过函数初始化)更合适,特别是当初始化逻辑较复杂或计算量较大时。
原因解释:
- 性能优化:函数初始化方式只会在组件首次渲染时执行一次,而直接初始化方式在每次渲染时都会创建新对象(虽然React会忽略后续的初始值)
- 避免重复计算:如果初始状态需要复杂计算,函数方式可以避免不必要的重复计算
- 大型对象适用:对于嵌套深、结构复杂的对象,函数初始化更加高效
拓展:但其实对于简单的初始状态(如数字、字符串等),两种方式差异不大,而且,在函数体内可以包含任何初始化逻辑,这种方式也适用于其他需要惰性初始化的场景。
useEffect
Q1: useEffect的依赖数组有什么作用?如何正确设置依赖项?
答案:
依赖数组决定了effect在什么条件下重新执行:
- 空数组[] :effect只在组件挂载和卸载时运行一次
- 不提供依赖数组:每次组件渲染后都会运行
- 包含特定依赖项:只有当这些依赖项的值发生变化时才会重新运行
正确设置依赖项的原则:
- 包含effect内部使用的所有会随时间变化的props和state
- 使用ESLint的exhaustive-deps规则可以帮助识别缺失的依赖项
- 如果某些值不需要触发effect重新运行,可以考虑使用useRef或useCallback来保持引用稳定
Q2:请解释以下 React 组件的行为,并说明 useEffect 的依赖数组如何影响其执行
题目代码:
js
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
useEffect(() => {
console.log('Effect ran - count changed');
return () => {
console.log('Cleanup from previous effect');
};
}, [count]);
useEffect(() => {
console.log('Effect ran - component mounted');
return () => {
console.log('Cleanup on unmount');
};
}, []);
useEffect(() => {
console.log('Effect ran - any state changed');
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
问题:
- 当组件首次渲染时,控制台会输出什么?
- 当点击"Increment"按钮时,控制台会输出什么?
- 当在输入框中输入文本时,控制台会输出什么?
- 当组件卸载时,控制台会输出什么?
答案
- 组件首次渲染时的输出
js
Effect ran - count changed
Effect ran - component mounted
Effect ran - any state changed
解析:
- 第一个 useEffect 由于 count 初始值为 0,首次渲染时会执行
- 第二个 useEffect 依赖数组为空,只在挂载时执行一次
- 第三个 useEffect 没有依赖数组,每次渲染后都会执行
- 点击"Increment"按钮时的输出
js
Cleanup from previous effect
Effect ran - count changed
Effect ran - any state changed
- count 状态改变,触发重新渲染
- 第一个 useEffect 依赖 count,所以先执行清理函数,然后执行 effect
- 第三个 useEffect 没有依赖数组,每次渲染后都会执行
- 第二个 useEffect 依赖数组为空,不会再次执行
- 在输入框中输入文本时的输出
js
Effect ran - any state changed
- text 状态改变,触发重新渲染
- 只有第三个 useEffect 没有依赖数组,会执行
- 其他两个 useEffect 的依赖项没有变化,不会执行
- 组件卸载时的输出
js
Cleanup from previous effect
Cleanup on unmount
- 组件卸载时会执行所有 useEffect 的清理函数
- 第一个和第二个 useEffect 有清理函数,会按声明顺序执行
- 第三个 useEffect 没有清理函数,不会输出任何内容
useLayoutEffct
Q1: 请解释以下代码中 useEffect
和 useLayoutEffect
的区别
jsx
import React, { useState, useEffect, useLayoutEffect } from 'react';
function FlashyComponent() {
const [value, setValue] = useState(0);
useEffect(() => {
console.log('useEffect - value changed:', value);
}, [value]);
useLayoutEffect(() => {
console.log('useLayoutEffect - value changed:', value);
}, [value]);
return (
<div>
<button onClick={() => setValue(v => v + 1)}>
Increment ({value})
</button>
</div>
);
}
- 当点击"Increment"按钮时,控制台输出的顺序是什么?为什么?
- 从用户的角度看,
useEffect
和useLayoutEffect
的执行有什么不同? - 在什么情况下应该优先使用
useLayoutEffect
?
答案
- 点击按钮时的输出顺序
js
useLayoutEffect - value changed: 1
useEffect - value changed: 1
useLayoutEffect
会在浏览器绘制屏幕之前同步执行useEffect
会在浏览器绘制屏幕之后异步执行- 因此
useLayoutEffect
总是先于useEffect
执行
- 优先使用
useLayoutEffect
的情况
- 当需要读取或修改 DOM 布局时(如测量元素尺寸、位置)
- 需要同步更新 DOM 以避免视觉闪烁时
- 执行动画或过渡效果时
Q2: 视觉上的白屏或闪烁是什么?如何解决白屏或闪烁
答案:
在 React 组件渲染过程中,如果 DOM 更新后,浏览器绘制前 出现短暂的 布局不一致 或 数据未加载完成 的情况,用户可能会看到:
- 白屏:组件渲染前短暂的无内容状态
- 闪烁:DOM 突然变化导致的视觉跳动(如高度、宽度突变)
如何解决白屏/闪烁?
使用 useLayoutEffect
避免布局跳动
适用场景:需要在用户看到 UI 前完成 DOM 调整(如测量元素尺寸、同步更新样式)。
示例 :避免 useEffect
导致的闪烁
jsx
function Tooltip() {
const [position, setPosition] = useState({ top: 0, left: 0 });
const ref = useRef();
useLayoutEffect(() => {
// 在浏览器绘制前同步计算位置
const { top, left } = ref.current.getBoundingClientRect();
setPosition({ top, left });
}, []);
return <div ref={ref} style={{ position: 'absolute', top: position.top }}>Tooltip</div>;
}
对比 useEffect
:
- 如果用
useEffect
,用户会先看到未定位的Tooltip
,再跳到正确位置(闪烁)。 useLayoutEffect
确保在绘制前完成定位,避免视觉跳动。
这里再提一提useEffect 和 useLayoutEffect 的执行时机对比
Hook | 执行时机 | 是否阻塞渲染 | 适用场景 |
---|---|---|---|
useEffect |
渲染完成 → 浏览器绘制 → 执行副作用 | ❌ 异步,不阻塞 | 数据获取、订阅、非紧急 DOM 操作 |
useLayoutEffect |
DOM 更新 → 浏览器绘制前 → 同步执行副作用 | ✅ 同步,阻塞渲染 | 需要 立即调整 DOM 的场景(避免布局跳动) |
useContext
Q1:请解释如何使用 React 的 useContext 来避免 props 层层传递
答案
js
import React, { createContext, useContext, useState } from 'react';
// 1. 创建一个 Context
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
// 2. 使用 Provider 提供值
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 3. 使用 useContext 获取值
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}
>
当前主题: {theme} (点击切换)
</button>
);
}
Q2: 使用Context时如何避免不必要的重新渲染?
答案:
- 拆分Context:将不常变化的值和频繁变化的值放在不同的Context中
- 使用memo:用React.memo包裹消费Context的子组件
- 使用useMemo:在Provider value中使用useMemo来记忆化值
- 选择性子订阅:可以使用类似于Redux的selector模式,只订阅需要的部分context值
jsx
const ThemeContext = React.createContext();
function App() {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
<MemoizedChild />
</ThemeContext.Provider>
);
}
useRef 相关问题
Q1: useRef和useState有什么主要区别?分别在什么场景下使用?
答案:
主要区别:
-
重新渲染:
- useState更新会触发组件重新渲染
- useRef更新不会触发重新渲染
-
可变性:
- useState的状态不可变,必须通过setState更新
- useRef的.current属性可以直接修改
-
同步性:
- useState的更新是异步的
- useRef的更新是同步的
使用场景:
-
useState:需要反映在UI上的状态
-
useRef:
- 访问DOM节点
- 存储不需要触发渲染的可变值
- 保存前一个状态或props的值
- 存储定时器ID或其他实例变量
Q2: 为什么在useEffect中需要使用useRef来存储定时器或事件监听器?
答案:
- 跨渲染周期持久化:useRef可以在组件重新渲染时保持相同的引用
- 清理副作用:在effect清理函数中可以访问到最新的ref值
- 避免闭包问题:使用ref可以避免在清理函数中访问到过期的闭包值
- 不触发额外渲染:修改ref不会导致组件重新渲染,适合存储与渲染无关的值
jsx
function TimerComponent() {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
// ...
}
总结
本文为Hooks相关的React面试问答系列,以后作者有空,还会陆续更新React中路由、模块化、组件通信等知识点的面试系列!尽情期待吧!