React Hooks的“记忆”与“行动”:useState 和 useEffect 的完美配合

75 阅读8分钟

React Hooks 初识:从 useState 到 useEffect

在现代前端开发中,React 函数式组件凭借其简洁、可读性强以及逻辑复用便利等优势,逐渐成为主流。而让函数式组件具备状态管理和副作用处理能力的关键,正是 React Hooks。本文将带你从最基础的 useState 开始,逐步理解 useEffect 的必要性与使用方式。

1. “记忆”从何而来?—— useState 如何让函数组件拥有状态

在类组件时代,我们通过 this.state 来管理组件内部的状态。而在函数式组件中,useState Hook 提供了同样的能力。

思考下面一段代码:

import { useState } from 'react';
export default function App(){
  
  const [num, setNum] = useState(1);
  return (
  <div onClick ={()=>setNum(num+1)}>
    
  )
}

这是最基础的usestate的用法:

我来简单解释一下:useState()接收一个初始值,我们这里设置为1,同时,它返回一个包含两个元素的数组第一个是当前状态的值(num)第二个是用于修改状态值的函数(setNum),我们在这里进行了解构操作,命名为num,setNum。

上面这段代码表示的逻辑可以用一张图来完美表示->

image.png

值得注意的是:event代指一系列能够引起状态值变化的事件,而不单单是点击事件

  1. 事件触发引起状态值改变
  2. 状态值的改变会导致页面的重新渲染(更新)
  3. 当然了,组件也可以引起一些事件执行,所以形成了三角关系

进阶用法

import { useState } from 'react';
export default function App(){

  const [num, setNum] = useState(()=>{
    const num1 =1+2;
    const num2 =2+3;
    return num1+num2;
  });

  return (
    // 修改函数中可以直接传新的值,也可以传入一个函数,这个函数的的参数是上一次的state
    <div onClick={()=>setNum((prevNum)=>{console.log(prevNum);return prevNum+1;})}>
      {num}
    </div>
  )
}

这段代码展示了 useState 的两种典型用法:

  • 惰性初始化:当初始状态需要通过复杂计算获得时,可以传入一个函数。React 会在组件首次渲染时调用该函数,并将其返回值作为初始状态。
  • 函数式更新:在调用 setNum 时传入一个函数,该函数接收前一次的状态作为参数。这在状态依赖于前一个状态时非常有用,能避免因闭包导致的状态“滞后”问题。

初始化传入的函数只能是一个纯函数
纯函数是指,当我们给一个函数一个固定的输入那么它一定会得到相同的输出,他是固定的能够确定的
比如:

const add  =function(x,y){
  fetch // 不确定的 
 return x+y;
}

如果x,y是需要通过fetch请求获得的,但是我们实际上并不能确定是否能够拿到结果,因为这个请求有可能失败,所以在useState中执行异步任务是不可取的。

useState 本身只负责状态的声明与更新。它无法处理“副作用”——比如数据请求、订阅、定时器等与 UI 渲染无关但又必须执行的操作。

但是实际开发中,我们的数据往往需要通过异步的请求来获取,但是useState做不到这点,这时候useEffect就登场了

二、副作用的引入:为什么需要 useEffect?

什么是副作用?简单来说,任何在组件渲染之外发生的、影响外部系统或依赖外部系统的行为,都属于副作用。

思考下面一段代码:

function add (nums){
  nums.push(3);
  return nums.reduce((pre,cur)=>pre+cur,0);
}
const nums = [1,2];
add(nums);// 副作用
console.log(nums.length);

它暗示了一个重要概念:如果函数在执行过程中修改了外部变量(如 nums.push(3)),或者依赖了非参数输入(如全局变量、网络请求等),那么它就不再是“纯函数”,也就产生了副作用。

在 React 中,组件理想上应是纯函数:给定 props 和 state,总是返回相同的 JSX。但现实应用中,我们常常需要:

  • 组件挂载后发起 API 请求;
  • 监听某些状态变化并作出响应;
  • 设置/清除定时器或事件监听器。

这些操作不能放在渲染逻辑中(会导致不可预测行为或性能问题),于是 useEffect 应运而生。

三、“行动”何时发生?—— useEffect 的三种典型执行时机

useEffect 是 React 提供的用于处理副作用的 Hook。它接收两个参数:一个副作用函数,以及一个可选的依赖数组。它由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);
    useEffect(()=>{
    console.log('xxx');
       queryData().then((data)=>{
             setNum(data);
        })
     },[])
     console.log('yyy');
    return (
        <div onClick={()=>setNum((prevNum)=>prevNum+1)}>
            {num}
        </div>
    )
}

我们这里使用一个定时器来模拟实际开发中的异步数据请求,但是在非严格模式下,控制台实际上只打印了一次‘xxx’,这里似乎并没有我们想看到的所谓的副作用,原因是什么呢?

实际上是我们传入的依赖项在暗暗发力,当我们传入一个空数组作为useEffect的依赖项时,它的含义就是,当前函数不依赖于任何东西,所以任何状态值的改变都无法触发这个副作用函数,它仅在函数挂载时才被调用一次。

这里还有一个值得注意的地方,我们在函数后想要打印‘yyy’,但是实际上,'yyy'却在'xxx'之前输出,这也是react的小细节,这些生命周期函数,会在组件挂载后执行,不与DOM争抢资源

这种用法通常用于数据的初始化

 queryData().then((data)=>{
             setNum(data);
        })

在依赖项变化时调用

我们替换useEffect为这样一段代码

  useEffect(()=>{
    console.log('zzz');
  },[num])

这次我们设置依赖项为num,当我们通过点击事件触发了num的更新时,就会调用useEffect方法,如果你学习过vue,那么可能会觉得这就像watch或者钩子函数onUpdate。

image.png

你现在应该已经了解了在状态更新时调用副作用函数的方式,那现在来考虑这样一段代码

 useEffect(()=>{
        console.log('effect');
        //一直执行的定时器
        const timer = setInterval(()=>{
            console.log(num); 
        },1000)
 },[num])

我们已经明白,当依赖项更新时,我们就会执行useEffect();那么这段代码就很显而易见了,每当我们更新了num时,就会新建一个一直运行的定时器,这会有一个什么后果呢

屏幕录制 2025-12-18 231711.gif 当我们点击更新的次数越来越多就会创建N个定时器,这显然不是我们想要的,那么我们要怎么做才能够销毁掉已经创建的定时器呢?

下一次执行前或卸载时调用

useEffect可以定义一个return函数(被称为清理函数),它能够在下一次执行uesEffect之前或者组件被卸载时被调用

 return ()=>{
    clearInterval(timer);
 }

我们在清理函数中进行定时器的销毁,就能确保调用这次的useEffect()之前,上一次的定时器被销毁

组件卸载时调用:

 return (
        <>
        <div onClick={()=>setNum((prevNum)=>prevNum+1)}>
            {num}
        </div>
        {num%2 === 0 &&<Demo/>}
        </>

我们这里在num为偶数时加载组件Demo,当组件被卸载时就会调用这个返回的函数 如果我们不对定时器进行回收,就会造成内存泄漏,导致页面崩溃

这对于定时器、事件监听、WebSocket 连接等场景至关重要。


小结

  • useState 赋予函数组件管理内部状态的能力,但它仅适用于同步、确定性的数据更新,无法处理异步操作或外部交互。
  • useEffect 则专门用于处理“副作用”——那些发生在渲染之外、与外部系统交互的操作,如数据请求、订阅、定时器等。
  • 通过依赖数组,我们可以精确控制副作用的执行时机:空数组表示仅在挂载时运行,包含状态则在该状态变化时重新执行。
  • 每次 useEffect 重新运行前,或组件卸载时,React 会自动调用其返回的清理函数(cleanup function) ,这是避免内存泄漏、确保资源正确释放的关键。

掌握 useStateuseEffect,就掌握了 React 函数式组件的核心心智模型。在此基础上,你将能更自信地构建复杂、健壮且可维护的现代前端应用。