React hooks 之 一篇文章掌握 useState 和 useEffect 的核心机制

引言:React Hooks 的核心价值

在 React 16.8 版本引入 Hooks 之前,React 函数组件是 "无状态" 的,开发者无法直接管理内部状态或执行副作用操作。如果需要使用这些能力,就必须使用类组件。然而,类组件语法冗长、逻辑复用困难、生命周期方法割裂业务逻辑,导致代码难以维护。

React Hooks 的出现彻底改变了这一局面。它允许开发者在不编写类的前提下,使用状态(state)和副作用(effects)等原本仅限于类组件的能力。

在众多内置的 Hooks 中,useStateuseEffect 是最基础、最常用的两个,堪称函数式 React 开发的两大"基石":

  • useState 赋予函数组件管理本地状态的能力,是构建交互式 UI 的起点;
  • useEffect 统一处理各种副作用逻辑,实现了声明式副作用管理。

因此,深入理解 useStateuseEffect,不仅是掌握 React 函数组件开发的关键,更是走向现代 React 工程开发的第一步。无论是构建简单计数器,还是复杂的数据驱动应用,这两个 Hooks 都扮演着不可或缺的角色。

第一部分:探索 useState

什么是 useState

useState 是 React 提供的一个 Hook(钩子) ,它让函数组件拥有了"记忆"能力

函数内的普通变量在函数执行后就会被销毁,如果我们下次再执行这个函数,中间的过程就需要重新执行一遍。

而利用 useState 机制"钩住"数据,此时数据就变成状态(state)了,即使重新运行函数,数据也状态也不会重置,除非卸载组件。

例如:

JSX 复制代码
import { useState } from 'react';

export default function CounterDemo() {
  // 普通变量:每次函数执行都会重置
  let normalCount = 0;

  // 状态变量:被 React "钩住",不会重置
  const [hookCount, setHookCount] = useState(0);

  // 每次点击,两个计数都尝试 +1
  const handleClick = () => {
    normalCount = normalCount + 1;        // 修改普通变量
    setHookCount(hookCount + 1);          // 更新状态
    console.log('普通变量:', normalCount);
    console.log('状态变量:', hookCount + 1); // 注意:这里还没重新渲染
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>普通变量 vs useState 状态</h2>
      <p>普通变量(每次渲染重置为 0): {normalCount}</p>
      <p>状态变量(持久保存): {hookCount}</p>
      <button onClick={handleClick}>点击 +1</button>
    </div>
  );
}

效果如下:

每次执行都会重新初始化所有变量,无法保留用户交互或数据变化的结果。但是通过 useState,我们可以:

  • 声明一个持久化的状态变量
  • 获取一个专门用于更新该状态的函数
  • 实现响应式 UI:当状态改变时,React 自动重新渲染组件,使视图与数据保持同步

核心价值useState 将函数组件从"一次性快照"转变为能随用户交互动态变化的活组件,状态(state)是变化的数据,也是组件的核心。


2. 基本使用方法

useState 的语法: const [num, setNum] = useState(初始值)

  • 状态变量 num:当前状态值

  • 更新变量的函数 setNum:更新状态的函数

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

function MyComponent() {
  // 解构赋值:[当前状态, 更新函数] = useState(初始值)
  const [num, setNum] = useState(1);
  
  return (
    <div onClick={() => setNum(num + 1)}>
      当前值: {num}
    </div>
  );
}

useState 支持两种初始化方式:

方式一:直接传入初始值 适用于简单、静态的初始值

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

方式二:传入初始化函数 当状态初始值需要经过复杂计算,就可以配置函数来计算

jsx 复制代码
const [num, setNum] = useState(() => {
  const num1 = 1 + 2;
  const num2 = 2 + 3; 
  return num1 + num2; // 返回 6
});

关键要求

  1. 函数必须为同步函数,异步的函数结果不确定,而状态一定要是确定的。
  2. 函数必须是纯函数:指每次传入相同的输入始终返回相同的输出,且无副作用的函数(不修改外部状态,不依赖外部状态,不改变传入的状态)

3. 状态更新机制

利用setState()函数进行状态更新不单单是"修改某个变量",并且触发了 React 的响应式更新循环

方式 代码示例 适用场景
直接传值 setNum(num + 1) 简单更新,不依赖旧状态
函数式更新 setNum(prev => prev + 1) 确保基于最新状态更新,避免闭包陷阱

React 的核心思想:数据驱动视图

React 的设计哲学可以用一句话精准概括:"视图是状态的映射,交互是改变状态的手段" ------ View = f(State)

而这就意味着:UI 界面完全由当前应用的状态(State)决定。只要状态相同,渲染出的界面就一定相同。这种"数据驱动视图"的模式,构成了 React 响应式更新机制的基础。


并且在 React 应用中,一切交互与渲染都围绕以下三个基本要素展开:

State(状态/数据)

  • 是应用的"心脏",存储着当前的数据(例如 num = 1)。
  • 在函数组件中,通常通过 useStateuseReducer 或状态管理库来管理。
  • State 必须被视为不可变 ------不能直接修改,只能通过 React 提供的 set 函数请求更新。

View(视图/UI)

  • 用户看到的界面,由 JSX 描述并最终转化为 DOM。
  • 组件本质上是一个函数:输入是 props 和 state,输出是 UI
  • 每次 state 改变,React 会自动重新执行组件函数,生成新的 View。

Event(事件/交互)

  • 用户对界面的操作,如点击按钮(onClick)、输入文本(onChange)等。
  • 事件处理函数是连接用户行为与状态更新的桥梁。

而基于这三个基本要素,形成了数据驱动的闭环流程

State → View:数据驱动显示

  • 含义:状态决定界面长什么样。
  • 机制:React 根据当前的 State 自动计算并渲染出对应的 View。

这是"声明式编程"的体现------你只需描述"UI 应该是什么",而不是"如何一步步操作 DOM"。

View → Event:视图产生交互

  • 含义:用户在界面上进行操作。
  • 机制:用户在 View 上交互(比如按钮)触发了 Event。

Event → State:事件改变数据

  • 含义:交互导致状态更新。
  • 机制 :事件处理函数调用 setState,向 React 提出状态变更请求。

⚠️ 注意:setState 是异步的,不会立即改变当前作用域中的 state 值,而是安排下一次重新渲染时使用新值。

最后闭环形成:自动重渲染(Re-render) :用户交互 → 触发事件 → 更新状态 → 驱动视图刷新

而这个闭环让我们无需手动操作 DOM,只需关注"状态如何变化",UI 便会自动同步。


4. 实践案例分析:点击计数器

jsx 复制代码
export default function App() {
  const [num, setNum] = useState(() => {
    const num1 = 1 + 2; 
    return num1; // 初始值 = 3
  });

  return (
    <div onClick={() => setNum((prevNum) => {
      console.log(prevNum); // 打印当前状态
      return prevNum + 1;   // 返回新状态
    })}>
      {num}
    </div>
  );
}

关键设计亮点

  • 函数式更新setNum(prev => ...) 确保即使多次快速点击,也能基于最新状态计算。
  • 纯函数初始化:初始值通过纯函数计算,符合 React 状态确定性原则。
  • 响应式核心 :完美体现 View = f(State) ------ 视图是状态的纯函数映射。

第二部分:解析useEffect

useEffect的作用

在React中,useEffect钩子用于处理副作用。而副作用指的是那些 影响外部世界或依赖于外部世界的操作,例如数据获取、订阅或者手动修改DOM等。

对于组件而言,理想情况下,输入参数应直接决定输出的JSX结构,而副作用则通过useEffect来处理(纯函数 <--对立--> 副作用)

基本使用方法

useEffect 的语法:useEffect(() => { return }, [])

useEffect的第一个参数是一个普通函数,通常使用简洁的箭头函数。

  • 包含需要执行的副作用逻辑(监听事件、启动定时器等),并且这个函数会在组件渲染到屏幕之后异步执行(不会阻塞浏览器绘制)
  • return函数 :清理函数,常用于清理上一次副作用(类似于Vue中的onUnmounted生命周期钩子)
jsx 复制代码
useEffect(() => {
  console.log('组件已渲染');
  const timer = setInterval(() => console.log('tick'), 1000);
  
  // 返回清理函数
  return () => {
    clearInterval(timer);
  };
});

useEffect的第二个参数是一个数组,称为依赖数组。决定了useEffect何时执行:

  • 空数组 [] :当没有提供依赖项时,useEffect仅在组件首次渲染后运行一次,并且不会随着后续的更新而重新触发(类似于Vue中的onMounted生命周期钩子)
  • 包含特定变量的数组[var] 或 [var1, var2...] :每当数组中的任何一个变量值改变时,都会触发useEffect的执行
  • 无数组(省略第二个参数) :如果省略了依赖数组,那么useEffect将在每次渲染之后都运行,包括初次挂载以及任何后续更新(类似于Vue中的onUpdated钩子)。

副作用清理

清理副作用是useEffect的一个十分重要的机制,如果操作不当很容易造成内存泄漏!!!

例如一个经典的定时器泄漏:

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

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

  useEffect(() => {
    // 定时器副作用
    // 每次执行useEffect都在新建定时器
    setInterval(() => {
      console.log(num);
    }, 1000);
  }, [num]);
  return (
    <>
      <div onClick={() => setNum((prevNum) => prevNum + 1)}>
        {num}
      </div>
    </>
  );
}

结果如图(我稍微更改了一下样式,看的更清楚)

在这里,每次num发生变化时,都会创建一个新的定时器,并且在下次num变化前没有及时清除旧的定时器,导致了内存泄漏。

useEffect内的函数进行修改,增加清理函数,在组件卸载或下一次执行useEffect前就会对副作用进行清理

jsx 复制代码
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(num);
    }, 1000);
    
    return () => {
      clearInterval(timer);// 清理资源
    }
  }, [num]);

只要你在 useEffect 中创建了"外部资源"或"长期运行的任务",就必须提供清理函数

结合实战:

在 React 中使用 useEffect + useState 实现异步数据获取并更新状态的标准模式

核心目的:让 useState 能响应异步数据

根据上文我们知道,useState 本身是同步的,不能直接"等待"异步操作。而 useEffect 允许我们在组件挂载后(或依赖变化时)执行副作用 ,包括调用异步函数

通过在 useEffect 中:

  1. 调用异步函数
  2. 在其 .then() 回调中调用 setNum(data)
  3. React 会自动触发重新渲染,使 UI 显示最新数据

这就实现了: "异步获取数据 → 更新状态 → 刷新视图" 的完整流程。


标准开发流程:异步数据获取 + 状态更新

1、使用 useState定义状态

用于存储异步获取的数据,以及可选的加载/错误状态

jsx 复制代码
const [num, setNum] = useState(0);          // 存储数据

2、封装异步数据获取函数

将数据请求逻辑抽离为独立函数

jsx 复制代码
async function queryData() {
  // 模拟网络请求
  const data = await new Promise(resolve => {
    // 异步执行后调用 resolve(666)
    setTimeout(() => resolve(666), 2000);
  });
  // 提取 Promise 的内部值 666 后,执行return返回数据
  return data;
}

3、在 useEffect 中调用异步函数(挂载时执行)

使用 useEffect 在组件挂载后发起请求,并在结果返回后更新状态。

jsx 复制代码
useEffect(() => {
  queryData().then(data => {
    // setNum(data) 触发重新渲染,使得组件函数再次执行,UI显示666
    setNum(data);
  });
}, []); // 空依赖数组:仅在挂载时执行一次

关键点

  • 依赖数组为 [],确保只执行一次(类似 Vue 的 onMounted
  • 不要直接在组件顶层写 await(组件函数必须是同步的!)

5、渲染 UI(使用状态值)

直接在 JSX 中使用状态变量:

JSX 复制代码
return (
  <div onClick={() => setNum(prev => prev + 1)}>
    {num} {/* 自动响应状态变化 */}
  </div>
);

setNum 被调用(无论是异步还是点击),React 会自动重新渲染组件。


完整标准代码示例

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

async function queryData() {
    const data = await new Promise(resolve => {
        setTimeout(() => {
            resolve(666);
        }, 2000);
    });
    return data;
}

export default function App() { 
    const [num, setNum] = useState(0);
    // 增加打印可视化代码执行时机
    console.log('yyy')

    useEffect(() => {
        // 增加打印可视化代码执行时机
        console.log('xxx')
        queryData().then(data => {
            setNum(data);
        })
    }, [])
    return (
        <>
            <div onClick={() => setNum(prevNum => prevNum + 1)}>
                {num}
            </div> 
        </>
    )
}

效果如下:

执行输出两次"yyy"和一次"xxx"的原因:

  • 首次页面挂载渲染,执行组件函数打印yyy,UI显示0
  • 当setNum()触发状态改变时重渲染,组件函数再次执行打印yyy,UI显示666

但是由于useEffect没有添加依赖项,所以只会在页面初次挂载时执行一次,所以打印一次xxx


执行流程时间线

时间 事件
T=0ms 组件挂载 → App() 执行 → num=0 → 渲染 0
T=0ms+ useEffect 执行 → 调用 queryData()
T=2000ms setTimeout 触发 → resolve(666)
T=2000ms+ .then 执行 → setNum(666)
T=2000ms++ 组件重新渲染 → 显示 666
相关推荐
Apifox.2 小时前
Apifox 12 月更新| AI 生成用例同步生成测试数据、接口文档完整性检测、设计 SSE 流式接口、从 Git 仓库导入数据
前端·人工智能·git·ai·postman·团队开发
bjzhang752 小时前
使用 HTML + JavaScript 实现滑动验证码
前端·javascript·html
行走的陀螺仪2 小时前
使用uniapp,实现根据时间倒计时执行进度条变化
前端·javascript·uni-app·vue2·h5
科技D人生2 小时前
Vue.js 学习总结(19)—— Vue3 按钮防重复点击三种方案总结
前端·vue.js·uniapp·vue3 防重复提交·uniapp 防重复提交·前端防抖
指尖跳动的光2 小时前
前端视角-如何保证系统稳定性
前端
fruge3 小时前
2025全栈技术深耕与实践:从框架融合到工程落地
前端
秋4273 小时前
tomcat与web服务器
服务器·前端·tomcat
hdsoft_huge3 小时前
Java 实现高效查询海量 geometry 及 Protobuf 序列化与天地图前端分片加载
java·前端·状态模式
MoonBit月兔3 小时前
用 MoonBit 打造的 Luna UI:日本开发者 mizchi 的 Web Components 实践
前端·数据库·mysql·ui·缓存·wasm·moonbit