React Hooks 详解:从 useState 到 useEffect,彻底掌握函数组件的“灵魂”

引言

在现代 React 开发中,函数组件 + Hooks 已经成为主流范式。它们不仅语法简洁、逻辑清晰,还赋予了函数组件过去只有类组件才拥有的能力:状态管理副作用处理生命周期控制 等。本文将结合实际代码,以最真实、最完整的代码为蓝本,深入浅出地讲解 React 中两个最核心的 Hooks:useStateuseEffect,并穿插解释一个至关重要的编程概念------纯函数

我们将严格引用代码,确保技术细节的准确性;同时用生动的语言和清晰的结构,带你从"知道怎么写"走向"真正理解为什么这样写"。


第一部分:useState ------ 函数组件的状态之源

什么是状态(State)?

在 React 中,状态(State)是驱动 UI 变化的数据 。没有状态,组件就是静态的、死板的。而 useState 就是让函数组件拥有状态的魔法钩子(Hook)。

我们来看 App2.jsx 的完整内容:

javascript 复制代码
// 状态管理 响应式数据,类似vue的ref
import { useState } from 'react'
export default function App() {
  // num是一个状态变量,初始值是1,setNum是更新num的函数
  // 数据通过setNum更新,变成了一个数据值,值不是固定的,而是一种状态 State
  // hook useState 为程序带来了关键的响应式状态
  // 状态是变化的数据,组件的核心是状态
  // const [num, setNum] = useState(1)

  const [num, setNum] = useState(() =>{
    // 如果初始值需要经过复杂的计算得到,用函数来计算
    // 一定要是同步函数,不支持异步函数
    // 异步的可能不确定有没有执行完,状态一定是确定的,所以不能用异步函数来计算初始值
    // 纯函数是指相同输入始终返回相同输出,而且没有副作用的函数
    const num1 = 1 + 2
    const num2 = 2 + 3
    return num1 + num2
  })

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

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

✅ 1. 直接传入初始值(简单场景)

scss 复制代码
const [num, setNum] = useState(1);
  • num 是当前状态值。
  • setNum 是更新状态的函数。
  • 当调用 setNum(newValue),React 会重新渲染组件,并用新值替换旧值。

✅ 2. 传入初始化函数(复杂计算场景)

dart 复制代码
const [num, setNum] = useState(() => {
  const num1 = 1 + 2
  const num2 = 2 + 3
  return num1 + num2
})

关键点 :这个函数只在组件首次渲染时执行一次,用于避免重复计算。

但注意注释中的警告:

"一定要是同步函数,不支持异步函数。异步的可能不确定有没有执行完,状态一定是确定的,所以不能用异步函数来计算初始值。"

这是因为 React 需要在渲染前就确定状态的初始值。如果用 async/await,函数会返回一个 Promise,而不是实际的值,这会导致状态变成 Promise {<pending>},显然不是我们想要的。


深入理解:为什么强调"纯函数"?

useState 的初始化函数注释中,有一句非常重要的话:

"纯函数是指相同输入始终返回相同输出,而且没有副作用的函数。"

为了理解这句话,我们来看看 1.js 中的例子:

javascript 复制代码
// function add(nums) {
//   nums.push(3)
//   return nums.reduce((pre, cur) => pre + cur, 0)
// }
// 改成纯函数 此时add函数没有副作用,作为赋值来使用
const add = function(x,y) {
  return x + y
}
const nums = [1, 2]
add(nums) // 副作用
console.log(nums.length)

原始版本(有副作用):

javascript 复制代码
function add(nums) {
  nums.push(3) // 修改了外部数组!
  return nums.reduce((pre, cur) => pre + cur, 0)
}
  • 这个函数不仅返回结果,还改变了传入的 nums 数组
  • 这种行为称为 副作用(Side Effect) :函数除了返回值,还影响了外部环境。

改写后(纯函数):

javascript 复制代码
const add = function(x, y) {
  return x + y
}
  • 给定相同的 xy,永远返回相同的和。
  • 不修改任何外部变量,不发起网络请求,不操作 DOM。
  • 这就是纯函数

为什么 React 要求初始化函数是纯函数?

因为 React 依赖可预测性。如果初始化函数有副作用(比如修改全局变量、调用 API、改变传入参数),那么:

  • 多次渲染可能导致不一致的状态。
  • 在服务端渲染(SSR)或 React.StrictMode 下可能出现 bug。
  • 调试变得极其困难。

因此,useState(() => {...}) 中的函数必须是纯函数:无副作用、确定性输出。


如何安全地更新状态?

App2.jsx 的 JSX 中,我们看到:

javascript 复制代码
<div onClick={() => setNum((prevNum) => {
  console.log(prevNum)
  return prevNum + 1
})}>
  {num}
</div>

这里 setNum 接收的是一个函数 ,而非直接的值。这种写法称为 "函数式更新"

为什么推荐这样做?

当状态更新依赖于前一个状态时(如计数器 +1),使用函数式更新可以避免竞态条件(race condition)。例如:

scss 复制代码
// 危险写法(可能出错)
setNum(num + 1);
setNum(num + 1); // 两次都基于同一个旧值!

// 安全写法
setNum(prev => prev + 1);
setNum(prev => prev + 1); // 每次都基于最新值

React 会将 prevNum 作为上一次提交的状态传入,确保你总是基于最新状态进行计算。


第二部分:useEffect ------ 副作用的总指挥

如果说 useState 赋予组件"记忆",那么 useEffect 就赋予组件"行动力"。

我们来看 App.jsx 的完整代码:

javascript 复制代码
import { useEffect, // 副作用
         useState // 响应式状态
} from 'react';
import Demo from './components/Demo';

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('useEffect 挂载前执行')

  // useEffect(() => {
  //   console.log('useEffect 挂载后执行')
  //   // 挂载后执行, vue生命周期onMounted
  //   queryData().then(data => {
  //     setNum(data);
  //   })
  // }, [])

  // 依赖项,只有依赖项变化时,才会执行副作用函数
  // }, [1,2,3]) // 依赖项为常数,只有在挂载时执行一次
  // }, [1,2,3,new Date()]) // 依赖项为对象,每次渲染时都执行

  // useEffect(() => {
  //   // 挂载时执行一次 onMounted
  //   // 更新时执行 onUpdated
  //   console.log(num, 'useEffect num 变化时执行')
  //   // num 变化时执行, vue生命周期onUpdated
  // }, [num])

  // useEffect(() => {
  //   console.log('如果依赖项为空数组,只有在挂载时执行一次')
  // }, [])

  // useEffect 定时器副作用
  // 内存泄漏 每次都在新建定时器,组件卸载时,定时器没有清除,会一直执行
  // 解决方法:在副作用函数中返回一个清除函数,组件卸载时,清除定时器
  // useEffect(() => {
  //   const timer = setInterval(() => {
  //     console.log('定时器副作用执行')
  //   }, 1000);
  //   // 重新执行effect之前,会先清除上一次的定时器
  //   // useEffect return 函数
  //   // 不清除上一次的定时器,会导致内存泄漏
  //   return () => {
  //     console.log('清除定时器')
  //     clearInterval(timer)
  //   }
  // }, [num])

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

这段代码通过大量注释,揭示了 useEffect 的三大核心用法。


场景一:模拟 onMounted ------ 组件挂载时执行一次

scss 复制代码
useEffect(() => {
  console.log('useEffect 挂载后执行')
  queryData().then(data => {
    setNum(data);
  })
}, [])
  • 依赖项为 [](空数组) :表示该 effect 只在组件挂载后执行一次

  • 相当于 Vue 的 onMounted 或类组件的 componentDidMount

  • 常用于:

    • 发起 API 请求
    • 订阅 WebSocket
    • 初始化第三方库

⚠️ 注意:虽然 queryData 是异步的,但 useEffect 本身不能是 async 函数(因为不能返回 Promise)。正确做法是在内部定义 async 函数并调用。


场景二:模拟 onUpdated / watch ------ 依赖变化时执行

scss 复制代码
useEffect(() => {
  console.log(num, 'useEffect num 变化时执行')
}, [num])
  • 依赖项为 [num] :只要 num 的值发生变化(通过 Object.is 比较),effect 就会重新执行。

  • 相当于 Vue 的 watch(num) 或类组件的 componentDidUpdate(配合条件判断)。

  • 适用于:

    • 根据搜索关键词发起请求
    • 监听路由变化
    • 动态调整 UI 行为

❗ 依赖项陷阱

  • [1, 2, 3]:全是常量 → 只执行一次(等同于 [])。
  • [new Date()]:每次渲染都生成新对象 → 每次都会执行(通常不是你想要的!)。
  • 忘记添加依赖 → 可能导致闭包捕获旧值(stale closure)。

React 官方推荐使用 ESLint 插件eslint-plugin-react-hooks)自动检测依赖项是否完整。


场景三:清理副作用 ------ 防止内存泄漏的关键!

这是 useEffect 最容易被忽视、却最重要的特性。

javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器副作用执行')
  }, 1000);

  return () => {
    console.log('清除定时器')
    clearInterval(timer)
  }
}, [num])

为什么需要清理?

  • 每次 num 变化,effect 会重新运行,创建新的定时器
  • 如果不清除旧的定时器,它们会继续在后台运行 → 内存泄漏
  • 更严重的是,如果组件卸载了,但定时器还在尝试 setState,React 会报 warning:"Can't perform a React state update on an unmounted component."

清理函数的作用时机:

  1. 下次 effect 执行前:先清理上一次的副作用。
  2. 组件卸载时:彻底清理所有资源。

这就是注释所说的:

"return函数 闭包,在下次执行effect前 或 组件卸载时执行"

这个返回的函数就是一个清理函数(cleanup function) ,它捕获了当前 effect 创建的资源(如 timer),形成闭包,确保能正确释放。


第三部分:Hooks 的哲学 ------ 纯函数 vs 副作用

"useEffect 副作用管理 effect副作用对立面是纯函数。对组件来说输入参数,输出jsx。"

这句话揭示了 React 函数组件的设计哲学:

  • 理想组件 = 纯函数:给定 props,总是返回相同的 JSX。
  • 现实需求 = 副作用:需要发请求、操作 DOM、订阅事件......
  • Hooks = 桥梁 :用 useEffect 把副作用"隔离"起来,保持组件主体的纯净。

这正是为什么:

  • useState 初始化要用纯函数(保证状态可预测)。
  • useEffect 专门处理副作用(不污染渲染逻辑)。

总结:React Hooks 的核心思想

Hook 作用 关键规则
useState 管理状态 初始化函数必须是同步纯函数;更新状态推荐使用函数式更新
useEffect 管理副作用 依赖项决定执行时机;必须提供清理函数防止内存泄漏

通过 App2.jsx,我们学会了如何用 useState 赋予组件状态,并理解了纯函数 的重要性;

通过 App.jsx,我们掌握了 useEffect 的三种经典模式:挂载执行依赖更新清理资源

通过 1.js,我们明白了副作用的危害纯函数的价值

通过 readme.md,我们看到了 Hooks 的整体图景。

项目源码链接:react/hooks· Zou/lesson_zp - 码云 - 开源中国

项目结构:


结语

React Hooks 不仅是一组 API,更是一种思维方式的转变 :将状态逻辑从生命周期中解耦,以声明式的方式组合行为。当你真正理解 useState 的纯函数约束、useEffect 的依赖机制与清理闭包,你就不再只是"会用 Hooks",而是掌握了一种构建健壮、可维护 React 应用的核心能力

现在,打开你的编辑器,把那些被注释掉的 useEffect 代码取消注释,亲自运行看看控制台输出吧!实践,才是理解 Hooks 的最佳路径。

相关推荐
RedHeartWWW5 小时前
Next.js Middleware 极简教程
前端
用户12039112947265 小时前
从零上手 LangChain:用 JavaScript 打造强大 AI 应用的全流程指南
javascript·langchain·llm
饼饼饼5 小时前
从 0 到 1:前端 CI/CD 实战 ( 第一篇: 云服务器环境搭建)
运维·前端·自动化运维
用户47949283569156 小时前
给前端明星开源项目Biome提 PR,被 Snapshot 测试坑了一把
前端·后端·测试
૮・ﻌ・6 小时前
小兔鲜电商项目(一):项目准备、Layout模块、Home模块
前端·javascript·vue
用户47949283569156 小时前
JavaScript 还有第三种注释?--> 竟然合法
javascript
dy17176 小时前
vue左右栏布局可拖拽
前端·css·html
zhougl9966 小时前
AJAX本质与核心概念
前端·javascript·ajax
hpz12236 小时前
对Element UI 组件的二次封装
javascript·vue.js·ui