React Hooks 深度解析
以下内容针对每个 Hook 从使用场景、注意事项、常见坑点、最佳实践四个维度进行详细讲解,配合完整代码示例。
1. useState 和 useReducer 的区别?
| 对比项 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单独立状态 | 复杂关联状态 |
| 更新方式 | setValue(newVal) |
dispatch({ type }) |
| 逻辑位置 | 分散在组件内 | 集中在 reducer 函数 |
| 可测试性 | 一般 | ✅ 纯函数易测试 |
| 性能优化 | 每次创建新函数 | dispatch 引用稳定 |
| 适合传递 | 不适合深层传递 | ✅ 配合 Context 传递 dispatch |
jsx
import { useState, useReducer } from 'react'
// ========== useState:简单状态 ==========
const Counter = () => {
const [count, setCount] = useState(0)
// ⚠️ 注意:useState 的更新是异步的,连续调用需要用函数形式
const handleTripleAdd = () => {
// ❌ 错误:count 只会 +1(三次都基于同一个旧值)
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// ✅ 正确:函数式更新,基于最新值
setCount(c => c + 1)
setCount(c => c + 1)
setCount(c => c + 1)
}
// 惰性初始化(只在首次渲染时执行)
const [expensiveState] = useState(() => {
return computeExpensiveValue() // 昂贵计算只执行一次
})
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={handleTripleAdd}>+3</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
)
}
// ========== useReducer:复杂关联状态 ==========
const initialState = { count: 0, step: 1, history: [] }
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + state.step, history: [...state.history, state.count] }
case 'DECREMENT':
return { ...state, count: state.count - state.step, history: [...state.history, state.count] }
case 'RESET':
return initialState
case 'SET_STEP':
return { ...state, step: action.payload }
case 'UNDO':
if (state.history.length === 0) return state
return { ...state, count: state.history.at(-1), history: state.history.slice(0, -1) }
default:
throw new Error(`Unknown action: ${action.type}`)
}
}
const AdvancedCounter = () => {
const [state, dispatch] = useReducer(reducer, initialState)
// dispatch 引用永远不变,传给子组件不会触发重渲染
return (
<div>
<p>Count: {state.count}, Step: {state.step}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'UNDO' })}>撤销</button>
<button onClick={() => dispatch({ type: 'SET_STEP', payload: 5 })}>步长5</button>
</div>
)
}
💡 面试加分点:
dispatch的引用是稳定不变的 (identity stable),传给子组件时不需要useCallback包裹------这是选择useReducer配合 Context 做状态管理的核心优势。useState内部其实就是useReducer的语法糖。📖 深入源码级解析 → 第 26 题(useState 深度解析)、第 27 题(useReducer 深度解析)
2. useState 返回值为什么必须用数组结构
-
React 内部实现决定的
useState返回的是一个元组(tuple),类型定义就是[S, Dispatch<SetStateAction<S>>]。React 团队选择数组形式是刻意为之的设计决策。
-
支持自定义 Hook 中灵活使用
- 如果返回对象 { state, setState },在自定义 Hook 里会非常别扭:
ts// ❌ 如果返回对象,每次都要 .state / .setState const { state, setState } = useCustomHook() // ✅ 数组解构可以随意重命名、跳过 const [value, setValue] = useCustomHook() const [ignored, setIgnored] = useAnotherHook() // 不用的也能方便忽略 -
顺序调用多个 Hook时无需关心属性名
ts// ✅ 数组 --- 简洁统一 const [name, setName] = useState('') const [age, setAge] = useState(0) const [email, setEmail] = useState('') // ❌ 对象 --- 如果多个 Hook 都返回 state/setValue 就会冲突 // const { state, setState } = useUser() // const { state, setState } = useOrder() // 命名冲突! -
底层原理:Hooks 链表存储
- React 内部通过链表存储每个 Hook 的状态,调用顺序必须一致。数组的索引天然对应链表的第 N 个 Hook:
codeHook1 → Hook2 → Hook3 [0] [1] [2] ← 数组索引 = Hook 调用顺序- 每次渲染时,React 按索引依次读取链表节点,useState 第一次调用返回 memoizedState, dispatch,后续调用通过 queue 更新。
-
扩展性考虑
- 未来 React 可能给 dispatch 增加额外属性或方法,数组形式不会影响已有用法;而对象形式一旦改了结构就是 breaking change。
- 一句话总结:数组解构是 React Hooks 的设计约定,兼顾了灵活性(任意重命名/跳过)、一致性(多 Hook 无命名冲突)和内部实现的简洁性(链表按索引对应)。
3. useEffect 的用法和注意事项?
| 依赖数组 | 执行时机 | 等价类组件生命周期 |
|---|---|---|
| 不传 | 每次渲染后 | componentDidMount + componentDidUpdate |
[] |
仅挂载后 | componentDidMount |
[dep1, dep2] |
dep 变化后 | componentDidUpdate(条件判断) |
| 返回清理函数 | 卸载/依赖变化前 | componentWillUnmount |
jsx
import { useState, useEffect, useLayoutEffect } from 'react'
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
// ========== 三种依赖形式 ==========
useEffect(() => { /* 每次渲染后执行(无依赖数组) */ })
useEffect(() => { /* 只在挂载时执行一次 */ }, [])
useEffect(() => {
// userId 变化时执行
let cancelled = false
setLoading(true)
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data)
setLoading(false)
}
})
// ✅ 清理函数:组件卸载或依赖变化前执行
return () => {
cancelled = true // 防止竞态条件(上一次请求未完成时 userId 又变了)
}
}, [userId])
// ========== 常见副作用清理 ==========
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000)
return () => clearInterval(timer) // 清理定时器
}, [])
useEffect(() => {
const handleResize = () => console.log(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) // 清理事件监听
}, [])
if (loading) return <div>Loading...</div>
return <div>{user?.name}</div>
}
// ========== useEffect vs useLayoutEffect ==========
// useEffect:异步执行,不阻塞绘制(绝大多数场景使用)
// useLayoutEffect:同步执行,在浏览器绘制前运行(DOM 测量/修改)
const Tooltip = ({ targetRef }) => {
const [pos, setPos] = useState({ top: 0, left: 0 })
// ✅ 用 useLayoutEffect:避免先渲染到错误位置再跳到正确位置
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect()
setPos({ top: rect.bottom + 5, left: rect.left })
}, [targetRef])
return <div style={{ position: 'fixed', ...pos }}>提示文字</div>
}
// ========== React 18 严格模式 ==========
// 开发环境下,React 18 会故意调用两次 useEffect(挂载→卸载→再挂载)
// 目的:验证你的清理函数是否正确
// ✅ 正确:有配对的清理
useEffect(() => {
const conn = createConnection()
conn.connect()
return () => conn.disconnect() // 清理
}, [])
// ❌ 错误:没有清理,严格模式下会建立两个连接
useEffect(() => {
const conn = createConnection()
conn.connect()
// 忘记 return 清理函数
}, [])
💡 面试加分点:
useEffect的清理函数不仅在卸载时执行,每次依赖变化导致重新执行前也会先执行上一次的清理 ------这是防止竞态条件的关键。useLayoutEffect的使用场景很少:只有需要在浏览器绘制前同步读取/修改 DOM 时才用(如测量元素位置、设置滚动位置)。📖 深入源码级解析 → 第 28 题(useEffect / useLayoutEffect / useInsertionEffect 深度对比)
4. useMemo 和 useCallback 的区别?
| 对比项 | useMemo | useCallback |
|---|---|---|
| 缓存目标 | 计算结果(值) | 函数引用 |
| 语法 | useMemo(() => compute(), [deps]) |
useCallback(fn, [deps]) |
| 等价关系 | --- | useCallback(fn, deps) = useMemo(() => fn, deps) |
| 使用场景 | 昂贵计算、引用稳定的对象 | 传给子组件的回调函数 |
| 配合使用 | 避免重复计算 | 配合 React.memo 避免子组件重渲染 |
jsx
import { useState, useMemo, useCallback, memo } from 'react'
const Parent = () => {
const [count, setCount] = useState(0)
const [items] = useState([
{ id: 1, name: 'A', value: 3 },
{ id: 2, name: 'B', value: 1 },
])
// ✅ useMemo:缓存计算结果(依赖不变则不重新计算)
const sortedItems = useMemo(() => {
console.log('重新排序...') // count 变化时不会执行
return [...items].sort((a, b) => a.value - b.value)
}, [items])
// ✅ useCallback:缓存函数引用(避免子组件不必要的重渲染)
const handleItemClick = useCallback((id) => {
console.log('点击了:', id)
}, []) // 空依赖 = 永远同一个函数引用
// ✅ useMemo 缓存对象(避免每次渲染创建新对象导致子组件重渲染)
const style = useMemo(() => ({ color: 'red', fontSize: 16 }), [])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
{/* count 变化时 MemoizedList 不会重渲染(因为 items/onClick 引用都没变) */}
<MemoizedList items={sortedItems} onItemClick={handleItemClick} style={style} />
</div>
)
}
// ========== React.memo:浅比较 props,不变则跳过渲染 ==========
const MemoizedList = memo(({ items, onItemClick }) => {
console.log('List 渲染')
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}: {item.value}
</li>
))}
</ul>
)
})
// ========== React.memo 自定义比较函数 ==========
const ExpensiveChart = memo(
({ data, config }) => {
// 渲染昂贵的图表
return <canvas />
},
(prevProps, nextProps) => {
// 返回 true 表示 props 相同,跳过渲染
// 返回 false 表示需要重渲染
return prevProps.data.length === nextProps.data.length
&& prevProps.config.type === nextProps.config.type
}
)
// ========== ⚠️ 什么时候不需要 useMemo/useCallback ==========
// ❌ 不需要:简单计算
const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName]) // 多此一举
const fullName2 = firstName + ' ' + lastName // ✅ 直接计算即可
// ❌ 不需要:不传给子组件的函数
const handleClick = useCallback(() => setCount(c => c + 1), []) // 如果没传给 memo 子组件就没意义
const handleClick2 = () => setCount(c => c + 1) // ✅ 直接写
💡 面试加分点: 过度使用
useMemo/useCallback反而有性能开销(闭包创建 + 依赖比较)。使用原则:只在「传给 memo 子组件的 props」或「真正昂贵的计算」时使用。React 19 的编译器(React Compiler)会自动进行记忆化,未来可能不再需要手动写这些 Hook。📖 深入源码级解析 → 第 30 题(useMemo / useCallback 深度解析)
5. useRef 的用法?
useRef 返回一个可变的引用对象({ current: ... }),在组件整个生命周期内保持不变。
jsx
import { useRef, useEffect, useState, useImperativeHandle, forwardRef } from 'react'
const InputFocus = () => {
// ========== 1. 访问 DOM 元素 ==========
const inputRef = useRef(null)
const handleFocus = () => inputRef.current?.focus()
// ========== 2. 保存不触发重渲染的值 ==========
const timerRef = useRef(null)
const renderCountRef = useRef(0)
const [running, setRunning] = useState(false)
// 每次渲染时计数(不会触发额外渲染)
renderCountRef.current++
const start = () => {
setRunning(true)
timerRef.current = setInterval(() => console.log('tick'), 1000)
}
const stop = () => {
setRunning(false)
clearInterval(timerRef.current)
}
// ========== 3. 保存上一次的值(usePrevious 模式) ==========
const [count, setCount] = useState(0)
const prevCountRef = useRef(0)
useEffect(() => {
prevCountRef.current = count // 渲染后更新,所以始终存的是"上一次"的值
})
// ========== 4. 在 useEffect 中访问最新 state(避免闭包陷阱) ==========
const latestCount = useRef(count)
latestCount.current = count // 每次渲染同步更新
useEffect(() => {
const timer = setInterval(() => {
console.log('最新 count:', latestCount.current) // ✅ 总是最新值
// 如果直接用 count,会永远是 0(闭包捕获了旧值)
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleFocus}>聚焦</button>
<p>当前: {count}, 上一次: {prevCountRef.current}</p>
<p>组件渲染了 {renderCountRef.current} 次</p>
</div>
)
}
💡 面试加分点:
useRef和useState的关键区别:修改 ref.current 不会触发重渲染 。常见用途:存储 DOM 引用、定时器 ID、上一次的值、以及解决闭包陷阱(在 useEffect 中通过 ref 访问最新 state)。📖 深入源码级解析 → 第 29 题(useRef / useImperativeHandle 深度解析)、第 32 题(闭包陷阱详解)
6. useState 深度解析
核心概念
useState 是最基础的状态 Hook,返回 [state, setState] 元组。
| 特性 | 说明 |
|---|---|
| 异步批处理 | 多次 setState 在同一事件中只触发一次渲染(React 18 所有场景) |
| 函数式更新 | setState(prev => prev + 1) 基于最新值更新 |
| 惰性初始化 | useState(() => compute()) 只在首次渲染时执行 |
| Object.is 比较 | 新值与旧值相同(Object.is)时跳过渲染 |
| 替换而非合并 | 与类组件的 this.setState 不同,不会自动合并对象 |
详细示例与注意事项
jsx
import { useState, useEffect } from 'react'
// ========== 1. 基础用法 & 函数式更新 ==========
const Counter = () => {
const [count, setCount] = useState(0)
// ❌ 错误:直接使用值更新,连续调用只会生效一次
const wrongTripleAdd = () => {
setCount(count + 1) // 基于闭包中的旧值 count=0
setCount(count + 1) // 还是 count=0,结果只 +1
setCount(count + 1) // 还是 count=0
}
// ✅ 正确:函数式更新,基于最新值
const rightTripleAdd = () => {
setCount(c => c + 1) // 0 → 1
setCount(c => c + 1) // 1 → 2
setCount(c => c + 1) // 2 → 3
}
return <button onClick={rightTripleAdd}>Count: {count}</button>
}
// ========== 2. 惰性初始化(Lazy Initialization) ==========
// ⚠️ 昂贵计算必须用惰性初始化,否则每次渲染都会执行
const ExpensiveComponent = ({ initialData }) => {
// ❌ 错误:每次渲染都执行 JSON.parse(即使结果被忽略)
const [data, setData] = useState(JSON.parse(localStorage.getItem('big-data')))
// ✅ 正确:传入函数,只在首次渲染执行
const [data2, setData2] = useState(() => {
console.log('只执行一次!')
const stored = localStorage.getItem('big-data')
return stored ? JSON.parse(stored) : initialData
})
// ✅ 另一个常见场景:初始化 Date 对象
const [startTime] = useState(() => new Date())
return <div>{JSON.stringify(data2)}</div>
}
// ========== 3. 对象/数组状态的不可变更新 ==========
const UserForm = () => {
const [user, setUser] = useState({
name: '',
age: 0,
address: { city: '', street: '' },
hobbies: ['reading']
})
// ❌ 错误:直接修改状态(不会触发重渲染!)
const wrongUpdate = () => {
user.name = 'Tom' // 直接修改引用类型
setUser(user) // 引用没变,React 认为没有变化,跳过渲染
}
// ✅ 正确:浅层属性更新(展开运算符)
const updateName = (name) => {
setUser(prev => ({ ...prev, name }))
}
// ✅ 正确:深层嵌套更新
const updateCity = (city) => {
setUser(prev => ({
...prev,
address: { ...prev.address, city }
}))
}
// ✅ 正确:数组操作(增删改)
const addHobby = (hobby) => {
setUser(prev => ({
...prev,
hobbies: [...prev.hobbies, hobby] // 添加
}))
}
const removeHobby = (index) => {
setUser(prev => ({
...prev,
hobbies: prev.hobbies.filter((_, i) => i !== index) // 删除
}))
}
const updateHobby = (index, value) => {
setUser(prev => ({
...prev,
hobbies: prev.hobbies.map((h, i) => i === index ? value : h) // 修改
}))
}
return (
<div>
<input value={user.name} onChange={e => updateName(e.target.value)} />
<input value={user.address.city} onChange={e => updateCity(e.target.value)} />
{user.hobbies.map((h, i) => (
<span key={i}>
{h} <button onClick={() => removeHobby(i)}>×</button>
</span>
))}
</div>
)
}
// ========== 4. 使用 Immer 简化深层更新 ==========
import { useImmer } from 'use-immer'
const UserFormWithImmer = () => {
const [user, updateUser] = useImmer({
name: '',
address: { city: '', street: '' },
hobbies: ['reading']
})
// ✅ Immer:直接"修改",内部自动生成新对象
const updateCity = (city) => {
updateUser(draft => {
draft.address.city = city // 看起来是直接修改,实际是不可变更新
})
}
const addHobby = (hobby) => {
updateUser(draft => { draft.hobbies.push(hobby) })
}
return <div>...</div>
}
// ========== 5. useState 的 bailout 机制(跳过渲染) ==========
const BailoutDemo = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(0) // 如果 count 已经是 0,React 用 Object.is 比较后跳过渲染
// ⚠️ 注意:React 可能仍然会渲染当前组件(但不渲染子树)
// 这叫 "eager bailout",是内部优化行为
}
console.log('渲染') // 第一次点击可能还会打印,但子组件不会更新
return <button onClick={handleClick}>{count}</button>
}
// ========== 6. 多个 useState vs 单个对象 ==========
// ✅ 推荐:相关状态放一起,无关状态分开
const [position, setPosition] = useState({ x: 0, y: 0 }) // 相关的放一起
const [isOpen, setIsOpen] = useState(false) // 无关的分开
const [selectedId, setSelectedId] = useState(null) // 无关的分开
// ❌ 不推荐:把所有状态塞进一个对象
const [state, setState] = useState({
position: { x: 0, y: 0 },
isOpen: false,
selectedId: null,
// 更新时要手动展开所有属性,容易遗漏
})
💡 面试加分点:
useState的 bailout 机制:当setState传入的值和当前 state 通过Object.is比较相同时,React 会跳过子组件渲染(但当前组件可能还会执行一次)。深层对象更新推荐用 Immer (useImmer)------它让你用"可变"的语法写出不可变更新代码。
7. useReducer 深度解析
核心概念
useReducer 是 useState 的替代方案,适合管理复杂状态逻辑。
| 适用场景 | 说明 |
|---|---|
| 多个关联状态 | 状态之间有依赖关系(如表单的 loading/error/data) |
| 复杂状态转换 | 有很多不同的操作类型 |
| 可预测性 | 纯函数 reducer 便于调试和测试 |
| 性能优化 | dispatch 引用稳定,配合 Context 传递不会导致重渲染 |
| 状态机模式 | 有明确的状态转换规则 |
详细示例与注意事项
jsx
import { useReducer, useCallback, createContext, useContext } from 'react'
// ========== 1. 完整的异步请求状态管理 ==========
// 定义 action 类型(TS 推荐用联合类型)
const ACTIONS = {
FETCH_START: 'FETCH_START',
FETCH_SUCCESS: 'FETCH_SUCCESS',
FETCH_ERROR: 'FETCH_ERROR',
RESET: 'RESET',
}
function fetchReducer(state, action) {
switch (action.type) {
case ACTIONS.FETCH_START:
return { ...state, loading: true, error: null }
case ACTIONS.FETCH_SUCCESS:
return { loading: false, data: action.payload, error: null }
case ACTIONS.FETCH_ERROR:
return { loading: false, data: null, error: action.payload }
case ACTIONS.RESET:
return { loading: false, data: null, error: null }
default:
throw new Error(`Unknown action: ${action.type}`)
}
}
// 封装成自定义 Hook
function useFetchReducer() {
const [state, dispatch] = useReducer(fetchReducer, {
loading: false,
data: null,
error: null,
})
const fetchData = useCallback(async (url) => {
dispatch({ type: ACTIONS.FETCH_START })
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
dispatch({ type: ACTIONS.FETCH_SUCCESS, payload: data })
} catch (err) {
dispatch({ type: ACTIONS.FETCH_ERROR, payload: err.message })
}
}, [])
return { ...state, fetchData, reset: () => dispatch({ type: ACTIONS.RESET }) }
}
// 使用
const UserList = () => {
const { data, loading, error, fetchData } = useFetchReducer()
useEffect(() => { fetchData('/api/users') }, [fetchData])
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// ========== 2. 复杂表单管理(多字段联动) ==========
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
// 修改字段时清除该字段的错误
errors: { ...state.errors, [action.field]: '' },
isDirty: true,
}
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error },
}
case 'SET_ERRORS':
return { ...state, errors: action.errors }
case 'SUBMIT_START':
return { ...state, isSubmitting: true }
case 'SUBMIT_SUCCESS':
return { ...state, isSubmitting: false, isDirty: false }
case 'SUBMIT_ERROR':
return { ...state, isSubmitting: false, submitError: action.error }
case 'RESET':
return { ...action.initialState, isDirty: false }
default:
return state
}
}
const RegistrationForm = () => {
const initialState = {
values: { username: '', email: '', password: '', confirmPassword: '' },
errors: {},
isSubmitting: false,
isDirty: false,
submitError: null,
}
const [state, dispatch] = useReducer(formReducer, initialState)
const setField = (field, value) => {
dispatch({ type: 'SET_FIELD', field, value })
}
const validate = () => {
const errors = {}
if (!state.values.username) errors.username = '用户名必填'
if (!state.values.email) errors.email = '邮箱必填'
else if (!/\S+@\S+\.\S+/.test(state.values.email)) errors.email = '邮箱格式不正确'
if (state.values.password.length < 6) errors.password = '密码至少6位'
if (state.values.password !== state.values.confirmPassword)
errors.confirmPassword = '两次密码不一致'
return errors
}
const handleSubmit = async (e) => {
e.preventDefault()
const errors = validate()
if (Object.keys(errors).length > 0) {
dispatch({ type: 'SET_ERRORS', errors })
return
}
dispatch({ type: 'SUBMIT_START' })
try {
await registerUser(state.values)
dispatch({ type: 'SUBMIT_SUCCESS' })
} catch (err) {
dispatch({ type: 'SUBMIT_ERROR', error: err.message })
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={state.values.username}
onChange={e => setField('username', e.target.value)}
placeholder="用户名"
/>
{state.errors.username && <span className="error">{state.errors.username}</span>}
<input
value={state.values.email}
onChange={e => setField('email', e.target.value)}
placeholder="邮箱"
/>
{state.errors.email && <span className="error">{state.errors.email}</span>}
<input
type="password"
value={state.values.password}
onChange={e => setField('password', e.target.value)}
placeholder="密码"
/>
{state.errors.password && <span className="error">{state.errors.password}</span>}
<input
type="password"
value={state.values.confirmPassword}
onChange={e => setField('confirmPassword', e.target.value)}
placeholder="确认密码"
/>
{state.errors.confirmPassword && <span className="error">{state.errors.confirmPassword}</span>}
{state.submitError && <div className="error">{state.submitError}</div>}
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? '注册中...' : '注册'}
</button>
<button type="button" onClick={() => dispatch({ type: 'RESET', initialState })}>
重置
</button>
</form>
)
}
// ========== 3. useReducer + Context(全局状态管理轻量方案) ==========
const TodoContext = createContext(null)
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }]
case 'TOGGLE':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t)
case 'DELETE':
return state.filter(t => t.id !== action.id)
case 'CLEAR_DONE':
return state.filter(t => !t.done)
default:
throw new Error(`Unknown action: ${action.type}`)
}
}
// Provider
const TodoProvider = ({ children }) => {
const [todos, dispatch] = useReducer(todoReducer, [])
// ✅ dispatch 引用永远不变,不需要 useMemo
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
)
}
// 子组件消费(任意层级)
const TodoItem = ({ todo }) => {
const { dispatch } = useContext(TodoContext)
// dispatch 引用稳定 → 配合 React.memo 可避免不必要的重渲染
return (
<li>
<input type="checkbox" checked={todo.done} onChange={() => dispatch({ type: 'TOGGLE', id: todo.id })} />
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>{todo.text}</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>删除</button>
</li>
)
}
// ========== 4. useReducer 的惰性初始化 ==========
function init(initialCount) {
// 昂贵的初始化逻辑
return { count: initialCount, history: [] }
}
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1, history: [...state.history, state.count] }
case 'RESET':
return init(action.payload) // 重新初始化
default:
return state
}
}
const Counter = ({ initialCount }) => {
// 第三个参数是 init 函数,接收第二个参数作为输入
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'RESET', payload: initialCount })}>重置</button>
</div>
)
}
💡 面试加分点:
useReducer的三个参数形式useReducer(reducer, initialArg, init)中,第三个init函数实现惰性初始化 ------只在首次渲染时调用,且方便实现"重置"功能。选择useStatevsuseReducer的经验法则:如果状态更新依赖前一个状态,或者有 3 个以上的 action 类型,就用useReducer。
8. useEffect / useLayoutEffect / useInsertionEffect 深度对比
三种 Effect Hook 执行时机
渲染流程:
render → DOM 变更 → useInsertionEffect → useLayoutEffect → 浏览器绘制 → useEffect
│ │ │ │ │
│ │ │ │ └── 异步,不阻塞绘制
│ │ │ └── 用户看到更新
│ │ └── 同步,阻塞绘制(DOM 测量/修改)
│ └── 同步,阻塞绘制(CSS-in-JS 注入样式)
└── React 计算虚拟 DOM
| Hook | 执行时机 | 是否阻塞绘制 | 使用场景 | 频率 |
|---|---|---|---|---|
| useEffect | 绘制后异步 | ❌ 不阻塞 | 数据请求、订阅、日志 | 99% 场景 |
| useLayoutEffect | 绘制前同步 | ⚠️ 阻塞 | DOM 测量、滚动位置、防闪烁 | 极少 |
| useInsertionEffect | DOM 变更后最先 | ⚠️ 阻塞 | CSS-in-JS 库注入样式 | 库作者用 |
详细示例与注意事项
jsx
import { useState, useEffect, useLayoutEffect, useInsertionEffect, useRef } from 'react'
// ========== 1. useEffect 常见场景全览 ==========
// 场景 A:数据请求(带竞态处理 + AbortController)
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const controller = new AbortController() // ✅ 推荐用 AbortController(比 cancelled 标志更彻底)
const fetchUser = async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(`/api/users/${userId}`, { signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
setUser(data)
} catch (err) {
if (err.name !== 'AbortError') { // 忽略取消的请求
setError(err.message)
}
} finally {
if (!controller.signal.aborted) {
setLoading(false)
}
}
}
fetchUser()
return () => controller.abort() // 清理:取消上一次请求
}, [userId])
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return <div>{user?.name}</div>
}
// 场景 B:事件监听
const WindowSize = () => {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight })
}
handleResize() // 初始化
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) // ✅ 清理
}, []) // 空依赖 = 只在挂载/卸载时执行
return <p>{size.width} × {size.height}</p>
}
// 场景 C:WebSocket 连接
const ChatRoom = ({ roomId }) => {
const [messages, setMessages] = useState([])
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/rooms/${roomId}`)
ws.onopen = () => console.log('已连接')
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
setMessages(prev => [...prev, msg]) // ✅ 函数式更新,避免闭包问题
}
ws.onerror = (err) => console.error('WebSocket 错误:', err)
return () => {
ws.close() // ✅ 清理:关闭连接
console.log('连接已关闭')
}
}, [roomId]) // roomId 变化 → 关闭旧连接 → 建立新连接
return <div>{messages.map((m, i) => <p key={i}>{m.text}</p>)}</div>
}
// 场景 D:定时器(setInterval)
const Timer = () => {
const [seconds, setSeconds] = useState(0)
const [isRunning, setIsRunning] = useState(false)
useEffect(() => {
if (!isRunning) return // 不运行时不设置定时器
const timer = setInterval(() => {
setSeconds(s => s + 1) // ✅ 函数式更新
}, 1000)
return () => clearInterval(timer) // ✅ 清理
}, [isRunning]) // isRunning 变化时重新设置
return (
<div>
<p>时间: {seconds}s</p>
<button onClick={() => setIsRunning(r => !r)}>
{isRunning ? '暂停' : '开始'}
</button>
<button onClick={() => { setIsRunning(false); setSeconds(0) }}>重置</button>
</div>
)
}
// 场景 E:IntersectionObserver(懒加载/无限滚动)
const LazyImage = ({ src, alt }) => {
const imgRef = useRef(null)
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect() // 加载后不再观察
}
},
{ threshold: 0.1 }
)
if (imgRef.current) observer.observe(imgRef.current)
return () => observer.disconnect() // ✅ 清理
}, [])
return (
<div ref={imgRef}>
{isVisible ? <img src={src} alt={alt} /> : <div className="placeholder" />}
</div>
)
}
// ========== 2. useEffect 依赖数组的常见坑 ==========
// ❌ 坑 1:对象/数组作为依赖(每次渲染都创建新引用)
const BadDeps = ({ userId }) => {
const options = { userId, page: 1 } // ❌ 每次渲染都创建新对象
useEffect(() => {
fetchData(options)
}, [options]) // ❌ 每次渲染 options 都是新引用 → 无限循环!
}
// ✅ 修复:拆解为原始值依赖
const GoodDeps = ({ userId }) => {
useEffect(() => {
fetchData({ userId, page: 1 })
}, [userId]) // ✅ userId 是原始值
}
// ❌ 坑 2:函数作为依赖
const BadFuncDep = ({ query }) => {
const fetchResults = () => fetch(`/api/search?q=${query}`) // ❌ 每次渲染都创建新函数
useEffect(() => {
fetchResults()
}, [fetchResults]) // ❌ 无限循环!
}
// ✅ 修复方案 1:把函数移到 useEffect 内部
const GoodFuncDep1 = ({ query }) => {
useEffect(() => {
const fetchResults = () => fetch(`/api/search?q=${query}`)
fetchResults()
}, [query]) // ✅ 依赖原始值
}
// ✅ 修复方案 2:useCallback 包裹
const GoodFuncDep2 = ({ query }) => {
const fetchResults = useCallback(() => {
return fetch(`/api/search?q=${query}`)
}, [query])
useEffect(() => {
fetchResults()
}, [fetchResults]) // ✅ query 不变 → fetchResults 不变
}
// ========== 3. useLayoutEffect 使用场景 ==========
// 场景 A:DOM 测量(避免闪烁)
const Tooltip = ({ children, targetRef }) => {
const tooltipRef = useRef(null)
const [position, setPosition] = useState({ top: 0, left: 0 })
// ✅ useLayoutEffect:在浏览器绘制前同步计算位置
// 如果用 useEffect,tooltip 会先渲染到 (0,0),再跳到正确位置(闪烁!)
useLayoutEffect(() => {
if (!targetRef.current || !tooltipRef.current) return
const targetRect = targetRef.current.getBoundingClientRect()
const tooltipRect = tooltipRef.current.getBoundingClientRect()
setPosition({
top: targetRect.bottom + 8,
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
})
}) // 无依赖 = 每次渲染后都重新计算
return (
<div ref={tooltipRef} style={{ position: 'fixed', ...position }}>
{children}
</div>
)
}
// 场景 B:自动滚动到底部(聊天消息)
const ChatMessages = ({ messages }) => {
const containerRef = useRef(null)
// ✅ useLayoutEffect:在绘制前滚动,用户看不到滚动过程
useLayoutEffect(() => {
const el = containerRef.current
if (el) {
el.scrollTop = el.scrollHeight
}
}, [messages])
return (
<div ref={containerRef} style={{ maxHeight: 400, overflow: 'auto' }}>
{messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
</div>
)
}
// 场景 C:防止 DOM 内容闪烁
const AnimatedHeight = ({ isExpanded, children }) => {
const contentRef = useRef(null)
const [height, setHeight] = useState(0)
// ✅ 在绘制前测量并设置高度,实现平滑动画
useLayoutEffect(() => {
if (contentRef.current) {
setHeight(isExpanded ? contentRef.current.scrollHeight : 0)
}
}, [isExpanded])
return (
<div style={{ height, overflow: 'hidden', transition: 'height 0.3s' }}>
<div ref={contentRef}>{children}</div>
</div>
)
}
// ========== 4. useInsertionEffect(CSS-in-JS 库作者专用) ==========
// ⚠️ 普通开发者几乎不需要使用!
// 用途:在 DOM 变更后、useLayoutEffect 之前注入 <style> 标签
// 场景:CSS-in-JS 库(如 styled-components、Emotion)需要在 DOM 测量前注入样式
useInsertionEffect(() => {
// 注入 CSS 规则
const style = document.createElement('style')
style.textContent = `.dynamic-class { color: red; }`
document.head.appendChild(style)
return () => {
document.head.removeChild(style)
}
}, [])
// ⚠️ 在 useInsertionEffect 中不能访问 refs,不能触发更新
// ========== 5. useEffect 清理函数的执行时机 ==========
const CleanupDemo = ({ id }) => {
useEffect(() => {
console.log(`Effect 执行: id=${id}`) // (2) 新的 effect 执行
return () => {
console.log(`清理: id=${id}`) // (1) 上一次的 effect 清理先执行
}
}, [id])
// 假设 id 从 1 变为 2:
// 输出顺序:
// "清理: id=1" ← 先清理旧的
// "Effect 执行: id=2" ← 再执行新的
return <div>ID: {id}</div>
}
💡 面试加分点:
useEffect是 99% 场景的选择------数据请求、订阅、日志等。useLayoutEffect只在需要同步测量/修改 DOM时使用(如 Tooltip 定位、滚动位置)------错误使用会导致性能问题。useInsertionEffect是 React 18 新增的,仅供 CSS-in-JS 库作者使用。- Effect 清理函数的执行顺序:先清理旧的 → 再执行新的,这是防止竞态条件的核心机制。
- 依赖数组中不要放对象/数组/函数 (每次渲染都是新引用),要么拆解为原始值,要么用
useMemo/useCallback稳定引用。
9. useRef / useImperativeHandle 深度解析
useRef 的本质
useRef 返回一个 { current: T } 对象,在组件的整个生命周期内保持同一引用,修改 .current 不触发重渲染。
| 使用场景 | 说明 |
|---|---|
| 访问 DOM 节点 | <input ref={inputRef} /> → inputRef.current |
| 存储不触发渲染的值 | 定时器 ID、上一次的 props/state、渲染次数等 |
| 解决闭包陷阱 | 在 useEffect/setTimeout 中访问最新值 |
| 跨渲染周期共享数据 | 类似类组件的实例变量 |
| 标识是否首次渲染 | isFirstRender.current |
详细示例
jsx
import { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
// ========== 1. DOM 操作大全 ==========
const DOMOperations = () => {
const inputRef = useRef(null)
const videoRef = useRef(null)
const canvasRef = useRef(null)
const scrollRef = useRef(null)
// 聚焦输入框
const focusInput = () => inputRef.current?.focus()
// 选中文本
const selectText = () => inputRef.current?.select()
// 滚动到元素
const scrollToBottom = () => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
}
// 视频控制
const playVideo = () => videoRef.current?.play()
const pauseVideo = () => videoRef.current?.pause()
// Canvas 绑定
useEffect(() => {
const ctx = canvasRef.current?.getContext('2d')
if (ctx) {
ctx.fillStyle = 'blue'
ctx.fillRect(10, 10, 100, 100)
}
}, [])
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦</button>
<button onClick={selectText}>全选</button>
<video ref={videoRef} src="video.mp4" />
<canvas ref={canvasRef} width={200} height={200} />
<div ref={scrollRef}>底部元素</div>
</div>
)
}
// ========== 2. 解决闭包陷阱(最常见的坑) ==========
const ClosureTrap = () => {
const [count, setCount] = useState(0)
// ❌ 闭包陷阱:setTimeout 中 count 永远是 0
const showCountBad = () => {
setTimeout(() => {
alert(`count = ${count}`) // 始终弹出 0(捕获了旧值)
}, 3000)
}
// ✅ 解决:用 ref 存储最新值
const countRef = useRef(count)
countRef.current = count // 每次渲染同步更新 ref
const showCountGood = () => {
setTimeout(() => {
alert(`count = ${countRef.current}`) // ✅ 总是最新值
}, 3000)
}
// ✅ 通用封装:useLatest Hook
// function useLatest(value) {
// const ref = useRef(value)
// ref.current = value
// return ref
// }
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={showCountBad}>3秒后弹出(❌ 旧值)</button>
<button onClick={showCountGood}>3秒后弹出(✅ 新值)</button>
</div>
)
}
// ========== 3. 存储上一次的值(usePrevious) ==========
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value // 渲染后更新,所以 ref.current 存的始终是"上一次"的值
})
return ref.current
}
const PreviousDemo = () => {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>当前: {count}, 上一次: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
// ========== 4. 首次渲染判断 ==========
function useIsFirstRender() {
const isFirst = useRef(true)
if (isFirst.current) {
isFirst.current = false
return true
}
return false
}
// 等价写法:跳过首次渲染执行 effect
function useUpdateEffect(effect, deps) {
const isFirst = useRef(true)
useEffect(() => {
if (isFirst.current) {
isFirst.current = false
return
}
return effect()
}, deps)
}
// 使用
const UpdateEffectDemo = ({ query }) => {
useUpdateEffect(() => {
// ✅ 只在 query 变化时执行(跳过首次渲染)
console.log('query 更新为:', query)
searchAPI(query)
}, [query])
return <div>搜索: {query}</div>
}
// ========== 5. 存储定时器/订阅等不需要触发渲染的值 ==========
const Stopwatch = () => {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const intervalRef = useRef(null) // ✅ 用 ref 存储定时器 ID
const startTimeRef = useRef(null) // 存储开始时间
const start = useCallback(() => {
setIsRunning(true)
startTimeRef.current = Date.now() - time
intervalRef.current = setInterval(() => {
setTime(Date.now() - startTimeRef.current)
}, 10)
}, [time])
const stop = useCallback(() => {
setIsRunning(false)
clearInterval(intervalRef.current)
}, [])
const reset = useCallback(() => {
setIsRunning(false)
clearInterval(intervalRef.current)
setTime(0)
}, [])
// 组件卸载时清理
useEffect(() => {
return () => clearInterval(intervalRef.current)
}, [])
return (
<div>
<p>{(time / 1000).toFixed(2)}s</p>
{isRunning ? (
<button onClick={stop}>暂停</button>
) : (
<button onClick={start}>开始</button>
)}
<button onClick={reset}>重置</button>
</div>
)
}
// ========== 6. Callback Ref(动态绑定 ref) ==========
// 当需要在 ref 绑定/解绑时执行逻辑时,使用 callback ref
const MeasureNode = () => {
const [height, setHeight] = useState(0)
// callback ref:节点挂载时调用,卸载时传入 null
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height)
}
}, [])
return (
<div>
<div ref={measuredRef} style={{ padding: 20 }}>
<p>Hello</p>
<p>World</p>
</div>
<p>上方元素高度: {height}px</p>
</div>
)
}
// ========== 7. useImperativeHandle:控制暴露给父组件的方法 ==========
// ⚠️ 原则:最小暴露,不要暴露整个 DOM
// 自定义 Video 播放器组件
const VideoPlayer = forwardRef(({ src, poster }, ref) => {
const videoRef = useRef(null)
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
useImperativeHandle(ref, () => ({
// ✅ 只暴露必要的方法
play: () => {
videoRef.current?.play()
setIsPlaying(true)
},
pause: () => {
videoRef.current?.pause()
setIsPlaying(false)
},
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time
}
},
getCurrentTime: () => videoRef.current?.currentTime ?? 0,
getDuration: () => videoRef.current?.duration ?? 0,
isPlaying: () => isPlaying,
// ❌ 不要暴露:getVideoElement() → 把整个 DOM 暴露出去不安全
}), [isPlaying]) // 依赖数组:isPlaying 变化时重新生成暴露的方法
return (
<video
ref={videoRef}
src={src}
poster={poster}
onTimeUpdate={() => setCurrentTime(videoRef.current?.currentTime ?? 0)}
/>
)
})
// 父组件使用
const VideoPage = () => {
const playerRef = useRef(null)
return (
<div>
<VideoPlayer ref={playerRef} src="movie.mp4" poster="poster.jpg" />
<button onClick={() => playerRef.current?.play()}>播放</button>
<button onClick={() => playerRef.current?.pause()}>暂停</button>
<button onClick={() => playerRef.current?.seek(30)}>跳到 30s</button>
<button onClick={() => alert(playerRef.current?.getCurrentTime())}>当前时间</button>
</div>
)
}
// ========== 8. React 19:不再需要 forwardRef ==========
// React 19+ 可以直接把 ref 作为普通 prop
function ModernInput({ ref, placeholder, ...props }) {
const innerRef = useRef(null)
useImperativeHandle(ref, () => ({
focus: () => innerRef.current?.focus(),
blur: () => innerRef.current?.blur(),
clear: () => { if (innerRef.current) innerRef.current.value = '' },
}))
return <input ref={innerRef} placeholder={placeholder} {...props} />
}
// 直接使用,无需 forwardRef
const App = () => {
const inputRef = useRef(null)
return <ModernInput ref={inputRef} placeholder="输入..." />
}
💡 面试加分点:
useRef最常被忽视的用法是解决闭包陷阱 :在useEffect/setTimeout/事件回调中通过 ref 读取最新的 state/props。useImperativeHandle遵循最小暴露原则------只暴露方法,不暴露 DOM 节点。- Callback ref (传函数作为 ref)适合需要在节点挂载/卸载时执行逻辑的场景(如动态测量尺寸)。
- React 19 中
ref可以作为普通 prop 传递,forwardRef将逐步废弃。
10. useMemo / useCallback 深度解析
核心区别
scss
useMemo(fn, deps) → 缓存 fn() 的返回值(计算结果)
useCallback(fn, deps) → 缓存 fn 本身(函数引用)
// 等价关系:
useCallback(fn, deps) === useMemo(() => fn, deps)
什么时候用?什么时候不用?
| 场景 | 需要用? | 说明 |
|---|---|---|
传给 React.memo 子组件的回调 |
✅ useCallback |
稳定引用,避免子组件重渲染 |
传给 React.memo 子组件的对象 prop |
✅ useMemo |
稳定引用 |
| 昂贵的计算(排序/过滤大数组) | ✅ useMemo |
避免重复计算 |
作为 useEffect 的依赖 |
✅ 两者 | 稳定引用,避免 effect 无限执行 |
| 简单的字符串拼接/数字计算 | ❌ 不需要 | 缓存开销 > 计算开销 |
| 不传给子组件的内部函数 | ❌ 不需要 | 没有意义 |
子组件没有用 React.memo |
❌ 通常不需要 | 父组件渲染子组件一定渲染 |
详细示例
jsx
import { useState, useMemo, useCallback, memo, useEffect } from 'react'
// ========== 1. useMemo:缓存昂贵计算 ==========
const ProductList = ({ products, category, sortBy }) => {
// ✅ 正确:过滤 + 排序大数组是昂贵操作
const filteredAndSorted = useMemo(() => {
console.log('重新过滤排序...') // 只在 products/category/sortBy 变化时执行
const filtered = products.filter(p => p.category === category)
return filtered.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price
if (sortBy === 'name') return a.name.localeCompare(b.name)
return 0
})
}, [products, category, sortBy])
// ❌ 不需要:简单计算
// const total = useMemo(() => filteredAndSorted.length, [filteredAndSorted])
const total = filteredAndSorted.length // ✅ 直接计算
return (
<div>
<p>共 {total} 个商品</p>
{filteredAndSorted.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
// ========== 2. useMemo:稳定对象引用 ==========
const ChartComponent = memo(({ config, data }) => {
console.log('Chart 渲染')
return <canvas />
})
const Dashboard = ({ theme }) => {
const [data, setData] = useState([])
// ❌ 错误:每次渲染都创建新对象 → ChartComponent 每次都重渲染
// <ChartComponent config={{ color: 'red', size: 100 }} data={data} />
// ✅ 正确:useMemo 缓存对象
const chartConfig = useMemo(() => ({
color: theme === 'dark' ? '#fff' : '#333',
size: 100,
grid: true,
animation: { duration: 300 },
}), [theme]) // 只有 theme 变化时重新创建
return <ChartComponent config={chartConfig} data={data} />
}
// ========== 3. useCallback:稳定函数引用(配合 React.memo) ==========
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log('TodoItem 渲染:', todo.id)
return (
<li>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
)
})
const TodoList = () => {
const [todos, setTodos] = useState([
{ id: 1, text: '学 React', done: false },
{ id: 2, text: '学 Hooks', done: true },
])
const [newTodo, setNewTodo] = useState('')
// ✅ useCallback:引用稳定,TodoItem 不会因为 TodoList 渲染而重渲染
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
}, []) // 空依赖:函数式更新不依赖外部变量
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id))
}, [])
const handleAdd = useCallback(() => {
if (!newTodo.trim()) return
setTodos(prev => [...prev, { id: Date.now(), text: newTodo, done: false }])
setNewTodo('')
}, [newTodo]) // ⚠️ 依赖 newTodo:不能用空依赖
return (
<div>
<input value={newTodo} onChange={e => setNewTodo(e.target.value)} />
<button onClick={handleAdd}>添加</button>
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
)
}
// ========== 4. useCallback 作为 useEffect 的依赖 ==========
const SearchComponent = ({ query }) => {
const [results, setResults] = useState([])
// ✅ 用 useCallback 稳定函数引用
const fetchResults = useCallback(async () => {
const res = await fetch(`/api/search?q=${query}`)
const data = await res.json()
setResults(data)
}, [query])
useEffect(() => {
fetchResults()
}, [fetchResults]) // query 变化 → fetchResults 变化 → effect 重新执行
return <div>{results.map(r => <p key={r.id}>{r.title}</p>)}</div>
}
// ========== 5. 常见误区:过度使用 ==========
const OverOptimization = () => {
const [count, setCount] = useState(0)
// ❌ 误区 1:简单计算不需要 useMemo
const doubled = useMemo(() => count * 2, [count]) // 多此一举
const doubled2 = count * 2 // ✅ 直接计算
// ❌ 误区 2:不传给 memo 子组件的函数不需要 useCallback
const increment = useCallback(() => setCount(c => c + 1), []) // 如果按钮不是 memo 包裹的,没意义
const increment2 = () => setCount(c => c + 1) // ✅ 更简洁
// ❌ 误区 3:子组件没用 React.memo,父组件的 useCallback 白费
return (
<div>
<p>{doubled2}</p>
<button onClick={increment2}>+1</button>
<ChildWithoutMemo onClick={increment} /> {/* ❌ Child 没用 memo,useCallback 无效 */}
</div>
)
}
// ========== 6. 完整的性能优化链路 ==========
// 1. 子组件用 React.memo 包裹
// 2. 传给子组件的函数用 useCallback
// 3. 传给子组件的对象/数组用 useMemo
// 这三步缺一不可!
const OptimizedParent = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('Alice')
// ✅ 步骤 2:useCallback 稳定函数引用
const handleClick = useCallback((id) => {
console.log('clicked', id)
}, [])
// ✅ 步骤 3:useMemo 稳定对象引用
const style = useMemo(() => ({
fontSize: 16,
color: 'blue'
}), [])
// ✅ useMemo 稳定数组引用
const items = useMemo(() => [
{ id: 1, label: 'React' },
{ id: 2, label: 'Vue' },
], [])
return (
<div>
{/* count 变化时,OptimizedChild 不会重渲染 */}
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<OptimizedChild name={name} items={items} style={style} onClick={handleClick} />
</div>
)
}
// ✅ 步骤 1:React.memo 包裹子组件
const OptimizedChild = memo(({ name, items, style, onClick }) => {
console.log('OptimizedChild 渲染')
return (
<div style={style}>
<h2>{name}</h2>
{items.map(item => (
<button key={item.id} onClick={() => onClick(item.id)}>{item.label}</button>
))}
</div>
)
})
// ========== 7. React 19 Compiler 的影响 ==========
// React Compiler(编译器)会在编译时自动进行记忆化
// 以下代码在 React 19 + Compiler 中等效于手动写 useMemo/useCallback
// React 19 + Compiler:自动优化,不需要手写
const FutureComponent = ({ items, category }) => {
// Compiler 自动检测到 filteredItems 依赖 items 和 category
// 自动添加等效于 useMemo 的缓存
const filteredItems = items.filter(i => i.category === category)
// Compiler 自动检测到 handleClick 可以被缓存
const handleClick = (id) => console.log(id)
return <MemoChild items={filteredItems} onClick={handleClick} />
}
💡 面试加分点:
useMemo/useCallback必须和React.memo配合使用才有意义------单独使用是没有效果的。- 性能优化的完整链路 :
React.memo(子组件) +useCallback(函数) +useMemo(对象/数组),三者缺一不可。- 不要过度优化 :
useMemo/useCallback本身有成本(闭包创建 + 依赖比较 + 内存占用),只在「传给 memo 子组件」或「昂贵计算」时使用。- React 19 的 React Compiler 会自动完成记忆化,未来可能不再需要手写这些 Hook。