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 的最佳路径。

相关推荐
mldong34 分钟前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排1 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰3 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen5 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马6 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu6 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss