从入门到实践:React Hooks 之 useState 与 useEffect 核心解析

作为前端开发者,React 的 Hooks 特性自推出以来,彻底改变了函数组件的开发模式,让我们能在无类组件的情况下轻松管理状态和处理副作用。本文将从最基础的useState开始,一步步深入到useEffect的使用逻辑,结合实际代码演进过程,带你理解 React Hooks 的核心思想与实践技巧。

一、起步:用 useState 给组件赋予响应式状态

useState 的核心特性总结

  • useState是 React 提供的状态钩子,用于在函数组件中管理状态。
  • 状态是 "变化的数据",是组件的核心,状态更新会触发组件重新渲染。
  • 初始值支持直接传值或函数式计算,函数式初始值适用于复杂计算场景。
  • 更新状态时,函数式更新能保证获取到最新的前序状态,是推荐的写法。

基本用法:初始化状态与更新状态

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

export default function App() {
  // 方式1:直接传入初始值
  // const [num, setNum] = useState(1);
  
  // 方式2:函数式初始化(适合复杂计算的初始值)
  const [num, setNum] = useState(() => {
    // 初始值需要复杂计算时,用函数计算更高效(仅执行一次)
    const num1 = 1 + 2;
    const num2 = 2 + 3;
    return num1 + num2; // 初始值为 8
  });

  return (
    <div onClick={() => setNum((prevNum) => {
      console.log(prevNum);
      return prevNum + 1;
    })}>
      <h1>当前数值:{num}</h1>
    </div>
  )
}
  • 传入一个无参函数 作为useState的参数,React 会在组件首次渲染时执行一次这个函数,并将函数的返回值作为状态的初始值。
  • 核心优势 : 若初始值需要复杂计算 (比如遍历数组、调用 API、大量数学运算),函数式初始化只会在组件挂载时执行一次 ;而如果直接写计算表达式(比如useState(1+2+2+3)),虽然结果一样,但该表达式会在每次组件重渲染时都执行一次(即使结果没用),造成性能浪费。

用表格总结核心区别

对比项 直接写计算表达式(useState(fn()) 函数式初始化(useState(fn)
执行时机 组件每次渲染时都执行 仅组件首次渲染时执行一次
返回值 函数的执行结果(比如数字、对象) 函数本身(React 自动执行)
性能 复杂计算时,重渲染会造成性能浪费 性能最优,无无效计算
适用场景 简单计算(比如useState(1+2) 复杂计算(比如本地存储读取、大数据处理)
  • setNum 接收一个函数 时,React 会把"当前最新的状态值"作为参数传进来( prevNum)。
  • 这种写法能避免闭包旧值的问题,确保总是基于"最新"状态做更新。
  • 每次点击,控制台先打印旧值,再把 num 加 1,页面自动重新渲染。(如上图所示)

纯函数要求 :React 推荐这个初始化函数是纯函数 ------ 是指相同输入始终返回相同的输出,且无副作用的函数。就像代码中计算 num1 + num2 的逻辑,没有修改外部变量,也没有依赖外部可变状态,每次执行都会得到 8,完全符合纯函数的特征。

另外要注意,这个初始化函数必须是同步函数,不支持异步操作。因为异步操作的结果是不确定的,而 React 要求状态的初始值必须是确定的,否则会导致组件初始化时的状态混乱。

这就是 React 的响应式状态核心:状态变化 → 组件重新渲染 → UI 更新。

二、进阶:用 useEffect 处理副作用

有了状态之后,我们需要处理组件的 "副作用"(比如数据请求定时器事件监听),这时候 useEffect 就成了核心工具。useEffect 可以看作是函数组件中生命周期的替代方案,但其灵活性远不止于此。

1. 先明确:什么是副作用?

在 React 中,副作用是指那些不属于组件渲染逻辑的操作,比如:

  • 数据请求(接口调用)、定时器 / 延时器
  • 操作 DOM、修改全局变量
  • 注册 / 取消事件监听

这些操作可能会影响外部环境,或依赖外部环境,而 useEffect 就是用来管理这些操作的。

2. useEffect 的核心特性:依赖项数组

useEffect 的第二个参数是依赖项数组 ,它决定了 useEffect 的执行时机,这也是 useEffect 最关键的特性。我们可以根据依赖项的不同,将 useEffect 分为三种常见场景,这也是我后续拆分代码的核心依据。

三、拆分学习:useEffect 的三种核心场景

为了更清晰地理解 useEffect 的用法,我将最初的代码拆分成了三个独立的组件,分别对应 useEffect 的三种典型使用场景。

1. 场景一:仅在组件挂载时执行(空依赖项)

这是最接近类组件 componentDidMount 的用法,依赖项数组为空 [] 时,useEffect 仅在组件挂载后执行一次,清理函数仅在组件卸载时执行。

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

// 模拟异步请求
async function queryData() {
  const data = await new Promise(resolve => {
    setTimeout(() => resolve(666), 2000);
  });
  return data;
}

export default function MountEffect() {
  const [num, setNum] = useState(0);

  // 空依赖项:仅挂载时执行一次
  useEffect(() => {
    console.log('组件挂载:Mounted');
    // 挂载后发起异步请求
    queryData().then(data => {
      setNum(data);
      console.log('异步请求完成,num更新为:', data);
    });

    // 清理函数:仅卸载时执行
    return () => {
      console.log('组件卸载:Unmounted');
    };
  }, []);

  return (
    <div>
      <h3>挂载阶段的 useEffect 演示</h3>
      <div>当前num值:{num}</div>
    </div>
  );
}

关键特点

  • 仅在组件挂载后执行一次,适合做一次性初始化操作(比如首次请求数据、注册全局事件)。
  • 清理函数仅在组件卸载时执行,用于销毁挂载时创建的副作用(比如清除定时器、取消事件监听)。
  • 注意闭包陷阱 :内部代码仅捕获首次渲染的状态,后续状态更新无法感知(如需获取最新状态,可使用 useRef)。

2. 场景二:依赖项变化时执行(带状态依赖)

当依赖项数组中包含状态(如 [num])时,useEffect 会在挂载时执行一次 ,之后每次依赖项变化时重新执行

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

export default function DepsEffect() {
  const [num, setNum] = useState(0);

  // 依赖项为 num:num变化时重新执行
  useEffect(() => {
    console.log('依赖项 num 变化:', '当前num:', num);
  }, [num]);

  console.log('组件渲染时的同步输出');

  return (
    <div>
      <h3>带依赖项的 useEffect 演示</h3>
      <div onClick={() => setNum(prev => prev + 1)}>
        点击修改num:{num}
      </div>
    </div>
  );
}

关键特点

  • 执行时机:组件渲染完成后异步执行,因此同步代码永远比 useEffect 先输出
  • 响应式:随依赖项的变化而更新,适合根据状态变化执行逻辑(比如状态变化时重新请求数据)。
  • 这是 React 实现 "响应式副作用" 的核心方式,类似 Vue 中的 watch

3. 场景三:清理函数(return 函数)

useEffect 中返回的函数是清理函数 ,用于销毁上一次的副作用,避免内存泄漏。它的执行时机是:下一次 useEffect 执行前组件卸载时

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

export default function CleanupEffect() {
  const [num, setNum] = useState(0);

  useEffect(() => {
    console.log('创建定时器:当前num:', num);
    // 创建定时器
    const timer = setInterval(() => {
      console.log('定时器输出 num:', num);
    }, 1000);

    // 清理函数:销毁上一次的定时器
    return () => {
      console.log('清除定时器:上一次的num:', num);
      clearInterval(timer);
    };
  }, [num]);

  return (
    <div>
      <h3>useEffect 清理函数演示</h3>
      <div onClick={() => setNum(prev => prev + 1)}>
        点击修改num:{num}
      </div>
    </div>
  );
}

关键特点

  • 清理函数的执行时机:依赖项变化时,先执行清理函数销毁旧副作用,再执行新的 useEffect 回调。
  • 避免内存泄漏:这是处理定时器、事件监听的标准方式,保证副作用随组件 / 状态的生命周期同步。
  • 闭包捕获:清理函数会捕获当前渲染的状态,因此能准确获取 "上一次的状态值"。

如果没有这个清理函数(return):

⚠这会导致内存泄漏!!!

四、React Hooks 学习总结

从最初的简单状态组件,到拆分后的三个核心场景,我们可以总结出 React Hooks 的几个核心思想:

1. 函数组件的核心:状态与副作用分离

  • useState 负责管理响应式状态,让组件能根据状态变化重新渲染。
  • useEffect 负责管理副作用,让渲染逻辑和副作用逻辑分离,代码更清晰。

2. useEffect 的核心:依赖项驱动

useEffect 的行为完全由依赖项数组决定,记住这几个规则:

  • 无依赖项:每次组件渲染后都执行。
  • 空依赖项:仅挂载时执行一次。
  • 有依赖项:挂载时执行一次,依赖项变化时重新执行。

3. 最佳实践

  1. 尽量使用函数式更新 :当新状态依赖旧状态时,用 setState(prev => newVal) 避免状态覆盖。
  2. 明确依赖项:不要省略必要的依赖项,否则会导致副作用与状态不同步(可借助 ESLint 插件检查)。
  3. 及时清理副作用:定时器、事件监听等一定要在清理函数中销毁,避免内存泄漏。
  4. 函数式初始化:初始值复杂时,用函数式初始化提高性能。
相关推荐
阿蒙Amon2 小时前
C#每日面试题-值类型与引用类型区别
java·面试·c#
山有木兮木有枝_2 小时前
当你的leader问你0.1+0.2=?
前端
前端程序猿之路2 小时前
模型应用开发的基础工具与原理之Web 框架
前端·python·语言模型·学习方法·web·ai编程·改行学it
名字被你们想完了2 小时前
Flutter 实现一个容器内部元素可平移、缩放和旋转等功能(八)
前端·flutter
听风说图2 小时前
Figma画布协议揭秘:组件系统的设计哲学
前端
sure2822 小时前
在react native中实现短视频平台滑动视频播放组件
前端·react native
weibkreuz2 小时前
React开发者工具的下载及安装@4
前端·javascript·react
代码猎人2 小时前
link和@import有什么区别
前端