React大厂面试问答系列之Hooks

前言

在React中,Hooks是非常重要的内容,很多人在学习或者备考的时候遇到各种各样的Hooks函数难免会头晕,太多了容易记混,这篇文章通过问答的格式来帮你巩固一下Hooks相关的考点,看看你能不能接住这些大厂中的常考问题吧!

useState

useState是最基础的Hook,用来得到 响应式状态 和 更新响应式状态的函数,使用方式为:

js 复制代码
const [state,setState] = useState("initival")

返回一个响应式状态state,其初始值为initval,还返回了更新状态的函数setState。下面就是useState相关的一些Q&A了

Q1: 请解释useState的基本用法,并写一个简单的计数器示例

答案:

useState是React提供的一个Hook,它允许我们在函数组件中添加和管理状态。基本用法如下:

jsx 复制代码
import React, { useState } from 'react';

function Counter() {
  // 声明一个叫count的状态变量,初始值为0
  // setCount是用来更新count的函数
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数: {count}</p>
      {/* 点击按钮时调用setCount更新状态 */}
      <button onClick={() => setCount(count + 1)}>
        点击增加
      </button>
    </div>
  );
}

这道题很基础了,用来小试牛刀,找找感觉

Q2:下面这段代码有什么问题?如何改进?

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={handleClick}>
        快速增加2
      </button>
    </div>
  );
}

答案:

点击按钮时,计数只会增加1而不是预期的2。这是因为React会批量处理状态更新,两次setCount都基于相同的count值。

改进方案 :

使用函数式更新,确保基于前一个状态值进行更新:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={handleClick}>
        快速增加2
      </button>
    </div>
  );
}

如果对这道题不熟悉的同学可以看看我之前写的文章:理解React中的状态批处理与更新队列,详细介绍了批处理机制!

Q3: 当我们需要在useState中初始化一个复杂对象时,哪种方式更合适?为什么?

题目代码:

jsx 复制代码
// 方式一:直接初始化
const [user, setUser] = useState({
  name: '张三',
  age: 25,
  preferences: {
    theme: 'dark',
    fontSize: 16
  }
});

// 方式二:通过函数初始化
const [user, setUser] = useState(() => ({
  name: '张三',
  age: 25,
  preferences: {
    theme: 'dark',
    fontSize: 16
  }
}));

答案

更合适的方式:方式二(通过函数初始化)更合适,特别是当初始化逻辑较复杂或计算量较大时。

原因解释:

  1. 性能优化:函数初始化方式只会在组件首次渲染时执行一次,而直接初始化方式在每次渲染时都会创建新对象(虽然React会忽略后续的初始值)
  2. 避免重复计算:如果初始状态需要复杂计算,函数方式可以避免不必要的重复计算
  3. 大型对象适用:对于嵌套深、结构复杂的对象,函数初始化更加高效

拓展:但其实对于简单的初始状态(如数字、字符串等),两种方式差异不大,而且,在函数体内可以包含任何初始化逻辑,这种方式也适用于其他需要惰性初始化的场景。

useEffect

Q1: useEffect的依赖数组有什么作用?如何正确设置依赖项?

答案:

依赖数组决定了effect在什么条件下重新执行:

  1. 空数组\[\] :effect只在组件挂载和卸载时运行一次
  2. 不提供依赖数组:每次组件渲染后都会运行
  3. 包含特定依赖项:只有当这些依赖项的值发生变化时才会重新运行

正确设置依赖项的原则:

  • 包含effect内部使用的所有会随时间变化的props和state
  • 使用ESLint的exhaustive-deps规则可以帮助识别缺失的依赖项
  • 如果某些值不需要触发effect重新运行,可以考虑使用useRef或useCallback来保持引用稳定

Q2:请解释以下 React 组件的行为,并说明 useEffect 的依赖数组如何影响其执行

题目代码:

js 复制代码
import React, { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  useEffect(() => {
    console.log('Effect ran - count changed');
    return () => {
      console.log('Cleanup from previous effect');
    };
  }, [count]);

  useEffect(() => {
    console.log('Effect ran - component mounted');
    return () => {
      console.log('Cleanup on unmount');
    };
  }, []);

  useEffect(() => {
    console.log('Effect ran - any state changed');
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <input 
        type="text" 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
      />
    </div>
  );
}

问题:

  1. 当组件首次渲染时,控制台会输出什么?
  2. 当点击"Increment"按钮时,控制台会输出什么?
  3. 当在输入框中输入文本时,控制台会输出什么?
  4. 当组件卸载时,控制台会输出什么?

答案

  1. 组件首次渲染时的输出
js 复制代码
Effect ran - count changed
Effect ran - component mounted
Effect ran - any state changed

解析:

  • 第一个 useEffect 由于 count 初始值为 0,首次渲染时会执行
  • 第二个 useEffect 依赖数组为空,只在挂载时执行一次
  • 第三个 useEffect 没有依赖数组,每次渲染后都会执行
  1. 点击"Increment"按钮时的输出
js 复制代码
Cleanup from previous effect
Effect ran - count changed
Effect ran - any state changed
  • count 状态改变,触发重新渲染
  • 第一个 useEffect 依赖 count,所以先执行清理函数,然后执行 effect
  • 第三个 useEffect 没有依赖数组,每次渲染后都会执行
  • 第二个 useEffect 依赖数组为空,不会再次执行
  1. 在输入框中输入文本时的输出
js 复制代码
Effect ran - any state changed
  • text 状态改变,触发重新渲染
  • 只有第三个 useEffect 没有依赖数组,会执行
  • 其他两个 useEffect 的依赖项没有变化,不会执行
  1. 组件卸载时的输出
js 复制代码
Cleanup from previous effect
Cleanup on unmount
  • 组件卸载时会执行所有 useEffect 的清理函数
  • 第一个和第二个 useEffect 有清理函数,会按声明顺序执行
  • 第三个 useEffect 没有清理函数,不会输出任何内容

useLayoutEffct

Q1: 请解释以下代码中 useEffect 和 useLayoutEffect 的区别

jsx 复制代码
import React, { useState, useEffect, useLayoutEffect } from 'react';

function FlashyComponent() {
  const [value, setValue] = useState(0);

  useEffect(() => {
    console.log('useEffect - value changed:', value);
  }, [value]);

  useLayoutEffect(() => {
    console.log('useLayoutEffect - value changed:', value);
  }, [value]);

  return (
    <div>
      <button onClick={() => setValue(v => v + 1)}>
        Increment ({value})
      </button>
    </div>
  );
}
  1. 当点击"Increment"按钮时,控制台输出的顺序是什么?为什么?
  2. 从用户的角度看,useEffect 和 useLayoutEffect 的执行有什么不同?
  3. 在什么情况下应该优先使用 useLayoutEffect?

答案

  1. 点击按钮时的输出顺序
js 复制代码
useLayoutEffect - value changed: 1
useEffect - value changed: 1
  • useLayoutEffect 会在浏览器绘制屏幕之前同步执行
  • useEffect 会在浏览器绘制屏幕之后异步执行
  • 因此 useLayoutEffect 总是先于 useEffect 执行
  1. 优先使用 useLayoutEffect 的情况
  • 当需要读取或修改 DOM 布局时(如测量元素尺寸、位置)
  • 需要同步更新 DOM 以避免视觉闪烁时
  • 执行动画或过渡效果时

Q2: 视觉上的白屏或闪烁是什么?如何解决白屏或闪烁

答案:

在 React 组件渲染过程中,如果 DOM 更新后,浏览器绘制前 出现短暂的 布局不一致 或 数据未加载完成 的情况,用户可能会看到:

  • 白屏:组件渲染前短暂的无内容状态
  • 闪烁:DOM 突然变化导致的视觉跳动(如高度、宽度突变)

如何解决白屏/闪烁?

使用 useLayoutEffect 避免布局跳动

适用场景:需要在用户看到 UI 前完成 DOM 调整(如测量元素尺寸、同步更新样式)。

示例 :避免 useEffect 导致的闪烁

jsx 复制代码
function Tooltip() {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const ref = useRef();

  useLayoutEffect(() => {
    // 在浏览器绘制前同步计算位置
    const { top, left } = ref.current.getBoundingClientRect();
    setPosition({ top, left });
  }, []);

  return <div ref={ref} style={{ position: 'absolute', top: position.top }}>Tooltip</div>;
}

对比 useEffect:

  • 如果用 useEffect,用户会先看到未定位的 Tooltip,再跳到正确位置(闪烁)。
  • useLayoutEffect 确保在绘制前完成定位,避免视觉跳动。

这里再提一提useEffect 和 useLayoutEffect 的执行时机对比

Hook 执行时机 是否阻塞渲染 适用场景
useEffect 渲染完成 → 浏览器绘制 → 执行副作用 ❌ 异步,不阻塞 数据获取、订阅、非紧急 DOM 操作
useLayoutEffect DOM 更新 → 浏览器绘制前 → 同步执行副作用 ✅ 同步,阻塞渲染 需要 立即调整 DOM 的场景(避免布局跳动)

useContext

Q1:请解释如何使用 React 的 useContext 来避免 props 层层传递

答案

js 复制代码
import React, { createContext, useContext, useState } from 'react';

// 1. 创建一个 Context
const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    // 2. 使用 Provider 提供值
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  // 3. 使用 useContext 获取值
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <button 
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
      style={{
        background: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff'
      }}
    >
      当前主题: {theme} (点击切换)
    </button>
  );
}

Q2: 使用Context时如何避免不必要的重新渲染?

答案:

  1. 拆分Context:将不常变化的值和频繁变化的值放在不同的Context中
  2. 使用memo:用React.memo包裹消费Context的子组件
  3. 使用useMemo:在Provider value中使用useMemo来记忆化值
  4. 选择性子订阅:可以使用类似于Redux的selector模式,只订阅需要的部分context值
jsx 复制代码
const ThemeContext = React.createContext();

function App() {
  const [theme, setTheme] = useState('light');
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  
  return (
    <ThemeContext.Provider value={value}>
      <MemoizedChild />
    </ThemeContext.Provider>
  );
}

useRef 相关问题

Q1: useRef和useState有什么主要区别?分别在什么场景下使用?

答案:

主要区别:

  1. 重新渲染:

    • useState更新会触发组件重新渲染
    • useRef更新不会触发重新渲染
  2. 可变性:

    • useState的状态不可变,必须通过setState更新
    • useRef的.current属性可以直接修改
  3. 同步性:

    • useState的更新是异步的
    • useRef的更新是同步的

使用场景:

  • useState:需要反映在UI上的状态

  • useRef:

    • 访问DOM节点
    • 存储不需要触发渲染的可变值
    • 保存前一个状态或props的值
    • 存储定时器ID或其他实例变量

Q2: 为什么在useEffect中需要使用useRef来存储定时器或事件监听器?

答案:

  1. 跨渲染周期持久化:useRef可以在组件重新渲染时保持相同的引用
  2. 清理副作用:在effect清理函数中可以访问到最新的ref值
  3. 避免闭包问题:使用ref可以避免在清理函数中访问到过期的闭包值
  4. 不触发额外渲染:修改ref不会导致组件重新渲染,适合存储与渲染无关的值
jsx 复制代码
function TimerComponent() {
  const intervalRef = useRef();
  
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);
    
    return () => clearInterval(intervalRef.current);
  }, []);
  
  // ...
}

总结

本文为Hooks相关的React面试问答系列,以后作者有空,还会陆续更新React中路由、模块化、组件通信等知识点的面试系列!尽情期待吧!

相关推荐
brien941 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance1 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M1 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写1 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding1 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马2 小时前
前沿视界
前端
Zan2 小时前
SQLite + SQLx 实战:为什么已经发布的数据库迁移文件,绝对不能再修改?
数据库·后端·面试
kyriewen2 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员