React Hooks 高频面试题

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 返回值为什么必须用数组结构

  1. React 内部实现决定的

    • useState 返回的是一个元组(tuple),类型定义就是 [S, Dispatch<SetStateAction<S>>]。React 团队选择数组形式是刻意为之的设计决策。
  2. 支持自定义 Hook 中灵活使用

    • 如果返回对象 { state, setState },在自定义 Hook 里会非常别扭:
    ts 复制代码
    // ❌ 如果返回对象,每次都要 .state / .setState
    const { state, setState } = useCustomHook()
    
    // ✅ 数组解构可以随意重命名、跳过
    const [value, setValue] = useCustomHook()
    const [ignored, setIgnored] = useAnotherHook() // 不用的也能方便忽略
  3. 顺序调用多个 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()  // 命名冲突!
  4. 底层原理:Hooks 链表存储

    • React 内部通过链表存储每个 Hook 的状态,调用顺序必须一致。数组的索引天然对应链表的第 N 个 Hook:
    code 复制代码
    Hook1 → Hook2 → Hook3
    [0]     [1]     [2]   ← 数组索引 = Hook 调用顺序
    • 每次渲染时,React 按索引依次读取链表节点,useState 第一次调用返回 memoizedState, dispatch,后续调用通过 queue 更新。
  5. 扩展性考虑

    • 未来 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>
  )
}

💡 面试加分点: useRefuseState 的关键区别:修改 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 会跳过子组件渲染(但当前组件可能还会执行一次)。深层对象更新推荐用 ImmeruseImmer)------它让你用"可变"的语法写出不可变更新代码。


7. useReducer 深度解析

核心概念

useReduceruseState 的替代方案,适合管理复杂状态逻辑。

适用场景 说明
多个关联状态 状态之间有依赖关系(如表单的 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 函数实现惰性初始化 ------只在首次渲染时调用,且方便实现"重置"功能。选择 useState vs useReducer 的经验法则:如果状态更新依赖前一个状态,或者有 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。

相关推荐
禁止摆烂_才浅1 小时前
React 高频面试题
前端·react.js·面试
禁止摆烂_才浅1 小时前
JavaScript 高级面试题
前端·javascript·面试
禁止摆烂_才浅1 小时前
JavaScript 基础 高频面试题
前端·javascript·面试
何时梦醒1 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强1 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
禁止摆烂_才浅2 小时前
HTML 高频面试题
前端·面试·html
一拳不是超人2 小时前
Godot 信号不是线程安全的:我是怎么在后台线程里翻车的
前端·架构
林太白2 小时前
What did Trea work help me with in this optimization process
前端·后端
一拳不是超人2 小时前
被 Tauri「体积小」种草后,我拿它做了个本地 AI 桌面工具,然后踩了这些坑
前端·架构