别再被生命周期绕晕了!带你丝滑入门 React Hooks:useState 与 useEffect 的正确打开方式

131 阅读6分钟

很高兴能和你一起探讨 React 的世界。如果你是从传统的 HTML/CSS/JS 走过来的,或者刚从 Vue 转型,那么 Hooks 绝对是让你爱上 React 的那个“瞬间”。

在 React 的世界里,Hooks(以 use 开头的函数)就像是给函数组件穿上的“钢铁侠战甲”,让原本只能负责渲染 UI 的简单函数,拥有了状态管理、生命周期钩子等超能力。

今天,我们就带上你的好奇心,深入拆解 React 中最核心的两个 Hook:useStateuseEffect


一、 为什么是 Hooks?

在 Hooks 出现之前,React 的状态逻辑必须写在 Class(类组件)里。但是类组件太重了,this 的指向问题经常让人头秃。

Hooks 的出现让 函数组件(Function Component) 变成了主流。它的风格非常“原生 JS”,代码看起来更简洁、更直观。

核心法则: 只有在 React 函数组件的最顶层或自定义 Hooks 中调用 Hooks,不要在循环、条件判断或嵌套函数中调用它们。


二、 useState:给组件注入“记忆”

组件本质上是一个函数。普通函数执行完,内部变量就销毁了。但 UI 需要“记忆”用户操作后的数据,这就是 State(状态) ,不固定的值就是状态,而状态组件的核心。。

1. 基础用法与延迟初始化

看下面这段代码,我们先聊聊 useState 的初始化:

JavaScript

import { useState } from 'react'

export default function App() {
  // 1. 初始化传入一个纯函数 (Lazy Initializer)
  // 关键点:如果初始值需要经过复杂的计算,不要直接写在 useState(calc()),
  // 这样每次组件重新渲染都会执行计算。传入一个纯函数,它只会在【首次挂载】时执行一次。
  const [num, setNum] = useState(() => {
    // 这里必须是同步函数,不能是 async/await
    // 纯函数:相同输入始终返回相同输出,无副作用
    const num1 = 1 + 2;
    const num2 = 2 + 3;
    return num1 + num2; // 最终初始值是 10
  });

  return (
    <div onClick={() => setNum(num + 1)}>
      {num}
    </div>
  )
}

一个很关键的点:初始化传入一个纯函数

什么是纯函数(Pure Function)?

简单来说,纯函数就是:相同的输入永远得到相同的输出,且没有副作用(不修改外部变量,不发请求)。

🤔 深度思考:为什么初始化不能是异步的?

React 的渲染过程必须是确定且连续的。如果初始状态是异步获取的(比如 fetch),在数据回来之前,React 不知道该渲染什么。因此,异步数据请求通常放在 useEffect 中,而 useState 只负责同步状态的定义。

2. 更新状态的两种姿势

当你调用 setNum 时,你有两种选择:

  • 直接传值: setNum(num + 1)。简单直观,但在处理高频连点或闭包场景时,可能会拿不到最新的 num

  • 传入函数(推荐)


   // 关键点:setState 也可以传入一个函数,参数是上一次的最新的 state (prevNum)

<div onClick={() => setNum((prevNum) => {

console.log("旧状态是:", prevNum);

return prevNum + 1;

})}>

{num}

</div>

专业术语:响应式状态 当你调用 setNum 时,React 会察觉到数据的变化,并自动重新触发函数组件的执行(重新渲染)。这就是 React 的响应式魔法。


三、 useEffect:处理那些“不安分”的副作用

在函数式编程中,我们追求 “纯函数”

  • 纯函数: 给它 xx,它永远返回 yy,且不修改外部变量(比如不发请求、不改 DOM)。
  • 副作用 (Side Effect): 凡是函数执行过程中对外部环境产生影响的操作,都叫副作用。比如:修改全局变量、发送网络请求、设置定时器、直接操作 DOM。

useEffect 就是 React 专门用来安置这些“副作用”的避风港。

1. 它的三张面孔(依赖项的区别)

useEffect(callback, dependencies),第二个参数数组 [] 决定了副作用执行的时机。

情况 A:不传依赖(每次都跑)

useEffect(() => {
    console.log('我是“劳模”,每次组件渲染(挂载+更新)后我都会执行');
});

情况 B:空数组依赖 [](只跑一次)

相当于类组件的 componentDidMount (onMounted)。

JavaScript

useEffect(() => {
    console.log('xxx');
    // 关键点:异步请求数据通常写在这里
    queryData().then(data => {
        setNum(data); 
    })
}, []); // 只在挂载完成时执行一次

情况 C:有依赖项 [num](按需执行)

相当于 componentDidUpdate (onUpdated)。

JavaScript

useEffect(() => {
    // 关键点:挂载时会执行一次,之后每当 num 发生变化时,这里都会再次执行
    console.log(num, 'zzz');
}, [num]); // 只有 num 变了,我才动

四、 核心重难点:生命周期与清理函数 (Cleanup)

很多新手会忽视 useEffect 的返回值。其实,这个 return 才是防止内存泄漏的关键。

1. 为什么要 Return?

看这个复杂的例子:

JavaScript

useEffect(() => {
    console.log('effect 执行了');
    
    // 关键点:设置定时器是一个典型的副作用
    const timer = setInterval(() => {
        console.log("当前的 num 是:", num);
    }, 1000);

    // 关键点:useEffect 的 return 函数是一个“闭包”
    // 它会在两个时机执行:
    // 1. 下一次 effect 执行之前
    // 2. 组件卸载 (Unmount) 时
    return () => {
        console.log('remove: 清理旧的副作用,防止内存泄漏');
        clearInterval(timer); // 重新执行前,先关掉旧的定时器
    }
}, [num]); 

执行流程拆解:

  1. num 从 0 变成 1。
  2. React 发现依赖项 num 变了。
  3. 第一步: 执行上一次 Effect 留下的 return 函数(清除旧定时器)。
  4. 第二步: 执行新的 Effect 函数(开启新定时器)。

如果没有这个 return,每次 num 改变你都会新开一个定时器,而旧的定时器还在后台默默运行。很快,你的浏览器就会卡死!

2. 组件卸载时的实战演练

看我们定义的那个 Demo 组件:

JavaScript

export default function Demo() {
    useEffect(() => {
        console.log('Demo 挂载了');
        const timer = setInterval(() => {
            console.log("Demo timer running...");
        }, 1000);

        return () => { 
            // 关键点:当 Demo 组件在父组件中因为条件判断被销毁时
            // 这个 return 函数会被调用,回收资源
            console.log('Demo 卸载,清理定时器');
            clearInterval(timer);
        }
    }, []) // 注意这里是空数组,意味着 return 只在组件真正销毁时执行
    
    return <div>偶数才会显示我哦</div>
}

在父组件中:{ num % 2 === 0 && <Demo /> }。

当 num 是奇数时,Demo 组件会从页面上消失。这时,useEffect 的清理函数会自动触发。这在处理 Socket 连接、全局事件监听、第三方库销毁时非常重要。


五、 纯函数 vs 副作用:技术深度思考

作为开发者,我们要时刻警惕代码中的“隐形破坏”。

什么是破坏?看这个例子:

JavaScript

function add(nums) {
    nums.push(3); // 关键点:这就是副作用!你修改了外部传入的引用类型
    return nums.reduce((pre, cur) => pre + cur, 0);
}

const nums = [1, 2];
add(nums);
console.log(nums.length); // 变成了 3!这就是不可预测性。

在 React 中,我们希望 组件渲染过程是纯净的

  1. 输入 Props,返回 JSX。
  2. 不要在渲染函数体(Function Body)内直接修改变量、发请求。
  3. 把所有不确定的事情,统统交给 useEffect

六、 总结与最佳实践

写好 React Hooks 的口诀:

  1. useState: 用来存“变”的数据。复杂初始值用函数传参,更新状态用回调函数获取 prev 值。

  2. useEffect: 用来处理“外”的事情。

    • []:挂载时跑一次(请求数据)。
    • [dep]:依赖变了跑一次(联动更新)。
    • return () => {}:随手关灯、随手关门(清理副作用)。

🎁 给新手的避坑指南:

  • 不要在 useEffect 里修改作为依赖项的那个状态(除非有终止条件),否则会陷入 死循环
  • 异步请求的数据,如果要在页面显示,一定要存进 useState