几道常见的 React Hooks 面试题,来看看你会吗?

232 阅读7分钟

1. 什么是 React Hooks?为什么出现?

Hooks 是 React 16.8 引入的特性,让函数组件也能使用 state 和生命周期逻辑。

Hooks 的出现解决了 class 组件代码容易臃肿的问题,比如一个生命周期里写了很多不相关逻辑,逻辑复用只能靠 HOC 或 Render Props,写起来嵌套复杂。

而 Hooks 能将内部逻辑抽出来复用,不用像以前那样需要写一堆 class 组件。

2. React Hooks 的执行顺序和依赖规则

执行顺序

Hooks 是按 读代码的顺序 记录和重用的——必须在函数最顶层调用(不能在 if/loop/嵌套函数里)。

React 会根据这种顺序把状态、effect、refs 绑定到组件上;每次渲染 React 都“按顺序”走一遍并复用之前的 Hook 槽位。

依赖规则

  1. 无依赖数组
useEffect(() => { 
  console.log('每次渲染都执行'); 
});

每次渲染后都执行。

  1. 空依赖数组
useEffect(() => { fetchData(); }, []); // 空数组

仅在首次渲染后执行一次(组件挂载时)。

  1. 包含依赖项
useEffect(() => { fetchUser(userId); }, [userId]); // 依赖 userId

userId 变化时执行(响应特定数据变化)。

3. 为什么 Hooks 不能写在条件语句或循环里?

因为 React 是靠调用顺序来匹配每个 Hook 的。 React 其内部有一个 Hook 链表,每次渲染都会顺序调用。如果在条件语句里写 Hook,下一次渲染可能顺序就乱了,导致 React 取错值。 所以 React 就干脆禁止了这种写法。

4. useState 的作用?state 更新是同步还是异步?

定义:useState 用来在函数组件中存储和更新状态。

  • useState更新是异步的,React 会合并多个 state 更新,一次性触发渲染。在React 18 以后在事件中是自动批量更新,避免了多次渲染浪费。

5. useEffect 和 useLayoutEffect 有什么区别?(useEffect 和 useLayoutEffect 的执行时机有何不同)?

  • useEffect 在渲染完成后执行
  • useLayoutEffect 在 DOM 更新后、浏览器绘制前执行

image.png

使用场景

  • useEffect → 非阻塞,适合异步操作(请求、日志)
  • useLayoutEffect → 会阻塞绘制,适合同步 DOM 操作

选取思路

  1. 默认选择 useEffect,适用于 99% 的副作用场景,性能最优
  2. 当出现视觉闪烁、布局跳动时考虑 useLayoutEffect
  3. useLayoutEffect 中操作要轻量,仅进行必要的 DOM 修改

6. useMemo 和 useCallback 的区别?什么时候用?如果滥用会怎么样?

useMemo

缓存计算结果,避免重复计算开销大的值

import { useMemo } from "react";

function MyComponent({ items }) {
  // 假设有一个计算密集型函数
  const computeExpensiveValue = (list) => {
    console.log("计算中...");
    return list.reduce((sum, item) => sum + item.value, 0);
  };

  // useMemo 用法
  const total = useMemo(() => computeExpensiveValue(items), [items]);

  return (
    <div>
      <p>Total: {total}</p>
    </div>
  );
}
  • 每次渲染时,如果 items 没变,React 会直接用上一次的 total,不会重复执行computeExpensiveValue
  • 实战场景:列表排序、筛选、统计、图表数据处理,或者任何需要“重计算”的场景。

useCallback

缓存函数引用,避免子组件重复渲染

import { useState, useCallback } from "react";

function Parent() {
  const [count, setCount] = useState(0);

  // 普通函数:每次渲染都会创建新函数
  // const handleClick = () => setCount(count + 1);

  // useCallback 版:使用函数式更新,避免依赖 count
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 依赖数组为空,函数引用永远不变

  return (
    <div>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
}

const Child = React.memo(({ onClick }) => {
  console.log("Child 渲染");
  return <button onClick={onClick}>+1</button>;
});
  • ChildReact.memo 包裹,如果 onClick 函数每次渲染都变,Child 会重复渲染。改用 useCallback 缓存函数引用,Child 就不会重复渲染了。
  • 常用于父组件状态频繁变化但子组件不想重渲染,比如列表中的按钮、表格操作列、卡片组件等。

滥用的后果 1.内存开销 增加每个 useMemo/useCallback 都需要内存来存储缓存 2.初始渲染性能下降 React 需要额外的工作来管理这些缓存,对于简单计算,缓存的开销可能比计算本身还大 3.代码复杂度增加 过多的 useMemo 和 useCallback 会降低可读性,需要费心管理依赖数组且容易遗漏依赖

7. useRef 是干嘛的,有哪些常见使用场景?

useRef 会返回一个 可变的对象 { current: ... } ,这个对象在组件的整个生命周期里保持不变,不会因为渲染而重置。

A. 获取 DOM 节点

  • 原理:React 会把 DOM 节点挂载到 ref 的 current 上。
  • 场景:直接操作 DOM,例如自动聚焦、滚动到某个位置或播放视频。

示例:自动聚焦输入框

import { useEffect, useRef } from "react";

function Login() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="用户名" />;
}

B. 存储跨渲染的可变变量

  • 原理:useRef 返回的对象在多次渲染间保持同一引用,不会触发重新渲染。
  • 场景
    1. 保存定时器 ID、WebSocket 实例等
    1. 保存前一次的状态(prevValue)
    1. 避免闭包问题(stale closure)

示例:保存定时器 ID

import { useEffect, useRef, useState } from "react";

function Timer() {
  const timerRef = useRef(null);
  const [count, setCount] = useState(0);

  useEffect(() => {
    timerRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(timerRef.current);
  }, []);

  return <p>{count}</p>;
}

C. 缓存上一次的值(prevValue)

  • 原理:通过 useRef 保存上一次渲染的值,在下一次渲染中访问。
  • 场景:对比当前值和上一次值,或者做过渡动画、判断状态变化等。

示例:缓存前一次值

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>当前: {count}</p>
      <p>上一次: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

8. 闭包陷阱

闭包陷阱是指当一个函数在 JavaScript 中“记住”了它创建时的作用域里的变量,但这个变量在后续更新了,函数仍然访问的是旧值,从而出现数据滞后的问题。

函数拿到的不是最新的值,而是它第一次闭包里捕获的那个值。

import { useState, useEffect } from "react";

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // ❌ 一直打印 0
      setCount(count + 1); // stale closure
    }, 1000);

    return () => clearInterval(id);
  }, []);
}

useEffect 的闭包捕获了 第一次渲染的 count 值(0)

后续 state 更新不会改变这个闭包里的 count ,所以计数器无法正确累加

正确写法

A. 函数式更新

setCount(c => c + 1);
  • 优点:永远拿到最新值
  • 不依赖闭包里的老值

B. useRef 保存最新值

const latestCount = useRef(count);

useEffect(() => { latestCount.current = count; }, [count]);

useEffect(() => {
  const id = setInterval(() => {
    console.log(latestCount.current);
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

9. 举一个自定义 Hook

自定义 Hook 就是把一些状态逻辑抽出来,形成可复用的函数。

  • 比如一个简单的防抖 useDebounce
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}
  • 场景:搜索框防抖,避免每次输入都触发请求

10. useContext 怎么用?和 Redux/Zustand 有什么区别?

useContext 用来拿到上层 Provider 提供的数据。

它适用于简单的全局状态,比如主题/语言;缺点是 Provider 值一变,所有子组件都会重渲染。

比如在项目里可以用 useContext 存用户登录信息(userInfo),比起 Redux 简单很多。 而 Redux/Zustand 更适合复杂状态,因为有中间件、持久化和性能优化

11. 什么是useReducer ?和 useContext 的区别?

这两个 Hook 其实解决的不是同一个问题:

  • useContext 的作用是数据传递。它能避免一层层传 props,直接把数据分发给子组件。但它本身并不关心数据怎么被修改,所以如果只用 useContext,一般会在里面放一个 state,再通过 setState 去改。这种方式在状态不复杂的时候挺方便,但逻辑会分散在各个组件里。
  • useReducer 则是管理数据更新逻辑的。它把 state 的变化统一放在 reducer 函数里,通过 action 来触发更新。这样一来,修改逻辑不会到处乱跑,状态流转路径清晰,尤其在状态复杂、有多种修改方式时更好维护。

所以如果只是单纯对比,useReducer 的优势在于逻辑集中、可预测

  • useReducer 统一管理状态和修改逻辑。
  • 再用 useContext 把 state 和 dispatch 往下传,这样子组件不用层层 props drilling,也不用各自到处写 setState。

可以说,useContext 解决的是“传”的问题,useReducer 解决的是“管”的问题,两者配合起来,就是 React 内置的轻量状态管理方案。