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: 请解释以下代码中 useEffectuseLayoutEffect 的区别

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. 从用户的角度看,useEffectuseLayoutEffect 的执行有什么不同?
  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中路由、模块化、组件通信等知识点的面试系列!尽情期待吧!

相关推荐
LaoZhangAI21 分钟前
Kiro vs Cursor:2025年AI编程IDE深度对比
前端·后端
止观止24 分钟前
CSS3 粘性定位解析:position sticky
前端·css·css3
爱编程的喵34 分钟前
深入理解JavaScript单例模式:从Storage封装到Modal弹窗的实战应用
前端·javascript
lemon_sjdk1 小时前
Java飞机大战小游戏(升级版)
java·前端·python
G等你下课1 小时前
如何用 useReducer + useContext 构建全局状态管理
前端·react.js
欧阳天羲1 小时前
AI 增强大前端数据加密与隐私保护:技术实现与合规遵
前端·人工智能·状态模式
慧一居士1 小时前
Axios 和Express 区别对比
前端
I'mxx1 小时前
【html常见页面布局】
前端·css·html
万少1 小时前
云测试提前定位和解决问题 萤火故事屋 上架流程
前端·harmonyos·客户端