从零开始学 React Hooks:useState 与 useEffect 核心解析

从零开始学 React Hooks:useState 与 useEffect 核心解析

作为 React 官方主推的语法,Hooks 让函数组件拥有了状态管理和生命周期的能力,彻底摆脱了类组件的繁琐语法,让 React 代码更贴近原生 JS。本文从纯函数与副作用 的基础概念出发,由浅入深讲解useStateuseEffect两个核心 Hooks 的使用,适合 JS 初学者快速上手,所有案例均基于实战代码拆解,易懂易练。

一、前置基础:纯函数与副作用

在学习 Hooks 前,必须先理解纯函数副作用这两个核心概念,它们是 Hooks 设计的底层逻辑,也是 React 组件设计的重要原则。

1.1 纯函数

纯函数是相同输入始终返回相同输出,且无任何副作用的同步函数,这是纯函数的三大核心特征:

  1. 输入确定,输出确定:不会因外部变量、环境变化改变返回结果
  2. 无副作用:不修改函数外部的变量、不操作 DOM、不发起网络请求等
  3. 必须同步:不包含异步操作(异步会导致返回结果不确定)

纯函数示例

ini 复制代码
// 纯函数:输入x和y,输出固定的和,无任何外部影响
const add = (x, y) => x + y;
// React中useState的初始值计算函数也是纯函数
const getInitNum = () => {
  const a = 1 + 2;
  const b = 2 + 3;
  return a + b; // 输入固定,返回值永远是8
};

1.2 副作用

副作用 是指函数执行过程中,对函数外部环境产生的一切影响,简单来说:非纯函数的操作,基本都是副作用

常见的副作用场景:

  • 修改函数外部的变量、数组、对象(如给数组 push 元素)
  • 发起网络请求(fetch/axios)、定时器 / 延时器(setTimeout/setInterval
  • 操作 DOM、本地存储(localStorage
  • 订阅 / 取消订阅事件

副作用示例

javascript 复制代码
// 有副作用:修改了外部的nums2数组
function add(nums2) {
  nums2.push(3); // 改变外部变量,副作用
  return nums2.reduce((pre, cur) => pre + cur, 0);
}
const nums2 = [1, 2];
add(nums2);
console.log(nums2); // [1,2,3],原数组被修改

// 有副作用:包含网络请求(不确定操作)
const add2 = (x, y) => {
  fetch('https://www.baidu.com'); // 网络请求,副作用
  return x + y;
};

1.3 组件与纯函数的关系

React 函数组件的核心逻辑 应该是纯函数:输入 props/state,输出固定的 JSX ,不包含副作用。而所有的副作用操作,都需要交给专门的 Hooks 来处理(如useEffect),这是 React 的设计规范,能保证组件的可预测性和稳定性。

二、useState:让函数组件拥有响应式状态

useState是 React 最基础的 Hooks,作用是为函数组件添加响应式状态,并提供修改状态的方法。状态(state)就是组件中会变化的数据,也是组件的核心,状态变化时,组件会自动重新渲染,更新页面内容。

2.1 基本使用

语法

javascript 复制代码
import { useState } from 'react';
// 解构赋值:state为当前状态值,setState为修改状态的方法
const [state, setState] = useState(initialValue);
  • initialValue:状态的初始值,可以是任意 JS 类型(数字、字符串、数组、对象等)
  • state:获取当前的状态值
  • setState:修改状态的方法,调用后会更新 state 并触发组件重新渲染

基础示例

javascript 复制代码
import { useState } from 'react'
export default function App(){
  // 初始化数字状态,初始值为1
  const [num, setNum] = useState(1);
  return (
    // 点击div,修改num状态
    <div onClick={() => setNum(num + 1)}>
      当前数字:{num}
    </div>
  )
}

点击页面中的 div,数字会逐次加 1,页面自动更新,这就是响应式状态的核心效果。

2.2 高级用法 1:函数式初始化

如果状态的初始值需要复杂计算 (如多个变量运算、循环处理),直接传值会导致每次组件渲染都重复计算,造成性能浪费。此时可以使用函数式初始化 ,该函数只会在组件首次挂载时执行一次,后续渲染不再执行。

语法

javascript 复制代码
// 传入纯函数,返回值作为初始值
const [state, setState] = useState(() => {
  // 复杂的同步计算逻辑(纯函数,无异步、无副作用)
  return 计算后的初始值;
});

实战示例

javascript 复制代码
import { useState } from 'react'
export default function App(){
  // 函数式初始化:仅首次挂载执行,计算初始值为8
  const [num, setNum] = useState(() => {
    const num1 = 1 + 2;
    const num2 = 2 + 3;
    return num1 + num2;
  });
  return (
    <div onClick={() => setNum(num + 1)}>
      初始值计算后:{num}
    </div>
  )
}

⚠️ 注意:初始化的函数必须是纯函数 ,不能包含异步操作(如setTimeout、网络请求),因为异步会导致初始值不确定,而 React 要求状态的初始值必须是确定的。

2.3 高级用法 2:函数式更新状态

修改状态时,setState不仅可以直接传入新值,还可以传入一个函数 ,该函数的参数是上一次的状态值,返回值为新的状态值。

适用场景 :当新的状态值依赖于上一次的状态值时,推荐使用函数式更新,能避免因 React 状态更新的异步性导致的取值错误。

语法

ini 复制代码
setState(preState => {
  // preState:上一次的状态值(React自动传入)
  return 新的状态值;
});

实战示例

dart 复制代码
import { useState } from 'react'
export default function App(){
  const [num, setNum] = useState(1);
  return (
    // 函数式更新:preNum为上一次的num值
    <div onClick={() => setNum((preNum) => {
      console.log('上一次的数字:', preNum);
      return preNum + 1; // 返回新值
    })}>
      当前数字:{num}
    </div>
  )
}

点击 div 时,会先打印上一次的数字,再返回新值,确保状态更新的准确性。

2.4 核心注意点

  1. useState必须在函数组件的顶层调用,不能在 if、for、嵌套函数中使用(React 通过调用顺序识别 Hooks)
  2. setState异步操作,调用后不能立即获取到新的状态值
  3. 状态更新是不可变的 :如果状态是对象 / 数组,不能直接修改原数据,需返回新的对象 / 数组(如setArr(pre => [...pre, newItem])

三、useEffect:处理组件的所有副作用

useEffect是 React 处理副作用 的核心 Hooks,作用是在函数组件中执行副作用操作,同时它还能模拟类组件的生命周期(如挂载、更新、卸载),让函数组件拥有了生命周期的能力。

3.1 基本概念

  • useEffect的直译是副作用效果,专门用来包裹组件中的所有副作用代码
  • 组件的核心逻辑(纯函数)负责渲染 JSX,副作用逻辑(请求、定时器、DOM 操作)全部放在useEffect
  • useEffect接收两个参数:副作用函数依赖项数组

3.2 基本语法

javascript 复制代码
import { useEffect } from 'react';
useEffect(() => {
  // 副作用函数:执行所有副作用操作(请求、定时器、DOM操作等)
  // 可选:返回一个清理函数
  return () => {
    // 清理函数:清除副作用(如清除定时器、取消订阅、关闭请求)
  };
}, [deps]); // 依赖项数组:控制useEffect的执行时机

3.3 三种使用场景(核心)

useEffect的执行时机完全由第二个参数(依赖项数组) 控制,分为三种核心场景,对应组件的不同生命周期阶段,这是useEffect的重点,一定要掌握!

场景 1:无依赖项数组 → 每次渲染都执行
javascript 复制代码
useEffect(() => {
  console.log('每次渲染/更新都会执行');
});
  • 组件首次挂载时执行一次
  • 组件每次状态更新 / 重新渲染时都会再次执行
  • 适用场景:需要实时响应组件所有变化的副作用(较少使用,注意性能)
场景 2:空依赖项数组 [] → 仅组件挂载时执行一次
scss 复制代码
useEffect(() => {
  console.log('仅挂载时执行,模拟onMounted');
  // 示例:挂载时发起异步请求
  queryData().then(data => setNum(data));
}, []);
  • 仅在组件首次挂载到 DOM时执行一次,后续无论状态如何更新,都不会再执行
  • 对应类组件的componentDidMount生命周期,是最常用的场景
  • 适用场景:初始化请求数据、初始化定时器、添加全局事件监听等
场景 3:有依赖项的数组 [state1, state2] → 依赖项变化时执行
scss 复制代码
const [num, setNum] = useState(0);
useEffect(() => {
  console.log('num变化时执行', num);
}, [num]); // 依赖项为num
  • 组件首次挂载时执行一次
  • 只有当依赖项数组中的值发生变化时,才会再次执行
  • 对应类组件的componentDidUpdate生命周期
  • 适用场景:依赖某个 / 某些状态的副作用(如状态变化时更新定时器、重新请求数据)

3.4 清理函数:清除副作用(避免内存泄漏)

useEffect的副作用函数可以返回一个清理函数 ,这是 React 的重要设计,用于清除副作用,避免内存泄漏。

清理函数的执行时机
  1. 当组件重新渲染 ,且useEffect即将再次执行时,先执行上一次的清理函数
  2. 当组件从 DOM 中卸载时,执行清理函数
核心使用场景:清除定时器 / 延时器

定时器是最常见的副作用,如果不及时清除,组件卸载后定时器仍会运行,导致内存泄漏,useEffect的清理函数完美解决这个问题。

实战示例

javascript 复制代码
import { useState, useEffect } from 'react'
export default function App() {
  const [num, setNum] = useState(0);
  useEffect(() => {
    console.log('num更新,创建新定时器');
    // 创建定时器:每秒打印当前num
    const timer = setInterval(() => {
      console.log(num);
    }, 1000);
    // 返回清理函数:清除上一次的定时器
    return () => {
      console.log('清除定时器');
      clearInterval(timer);
    };
  }, [num]); // 依赖num,num变化时执行

  return (
    <div onClick={() => setNum(pre => pre + 1)}>
      点击修改num:{num}
    </div>
  )
}

执行效果

  1. 组件挂载时,创建定时器,每秒打印 num
  2. 点击 div 修改 num,useEffect先执行清理函数清除旧定时器,再创建新定时器
  3. 组件卸载时,执行清理函数清除定时器,避免内存泄漏
其他清理场景
  • 取消网络请求(如 AbortController)
  • 移除全局事件监听(如window.removeEventListener
  • 取消订阅(如 Redux 订阅、WebSocket 订阅)

3.5 实战:结合 useEffect 实现异步请求初始化数据

前面提到,useState的函数式初始化不支持异步,因此组件挂载时的异步请求数据 ,需要结合useEffect(空依赖)实现,这是项目中的高频用法。

实战示例

javascript 复制代码
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(() => {
    queryData().then(data => {
      setNum(data); // 请求成功后修改状态,更新页面
    });
  }, []);

  return <div>接口返回数据:{num}</div>;
}

组件挂载后,发起异步请求,请求成功后修改num状态,页面自动更新为接口返回的 666。

四、Hooks 的通用使用规则

除了useStateuseEffect,React 所有的 Hooks(包括自定义 Hooks)都遵循以下两条核心规则,这是 React 官方强制要求的,违反会导致组件运行异常:

4.1 只能在函数组件 / 自定义 Hooks 中调用

Hooks 只能在React 函数组件 的顶层,或者自定义 Hooks中调用,不能在普通 JS 函数、类组件中使用。

4.2 只能在顶层调用,不能嵌套

Hooks 不能在 if、for、while、嵌套函数(如 useEffect 的副作用函数)中调用,必须在函数组件的顶层作用域 调用。因为 React 通过调用顺序来识别和管理每个 Hooks 的状态,如果嵌套调用,会导致调用顺序混乱,Hooks 状态失效。

五、实战综合案例:条件渲染 + 副作用清理

结合useState的状态管理、useEffect的副作用处理、React 的条件渲染,实现一个完整的小案例,覆盖本文所有核心知识点:

  1. 点击页面修改数字状态,数字为偶数时渲染Demo组件,奇数时卸载
  2. Demo组件挂载时创建定时器,卸载时清除定时器
  3. 主组件的数字变化时,更新定时器并实时打印

主组件 App.jsx

javascript 复制代码
import { useState, useEffect } from 'react'
import Demo from './Demo';
export default function App() {
  const [num, setNum] = useState(0);
  // 依赖num的副作用,处理定时器
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('当前num:', num);
    }, 1000);
    return () => clearInterval(timer);
  }, [num]);

  // 条件渲染:num为偶数时渲染Demo组件
  return (
    <div onClick={() => setNum(pre => pre + 1)} style={{ fontSize: '24px' }}>
      点击修改数字:{num}
      {num % 2 === 0 && <Demo />}
    </div>
  )
}

子组件 Demo.jsx

javascript 复制代码
import { useEffect } from 'react'
export default function Demo() {
  // 空依赖:仅挂载时创建定时器,卸载时清除
  useEffect(()=>{
      console.log('Demo组件挂载');
      const timer=setInterval(()=>{
          console.log('Demo组件的定时器');
      },1000)
      // 组件卸载时执行,清除定时器
      return ()=>{
          console.log('Demo组件卸载,清除定时器');
          clearInterval(timer)
      }
  },[])
  return <div style={{ marginTop: '20px' }}>我是偶数时显示的Demo组件</div>
}

案例效果

  1. 初始 num=0(偶数),渲染 Demo 组件,Demo 挂载并创建定时器
  2. 点击一次 num=1(奇数),卸载 Demo 组件,执行 Demo 的清理函数清除定时器
  3. 每次点击修改 num,主组件的useEffect都会先清除旧定时器,再创建新定时器
  4. 组件卸载时,所有定时器都会被清除,无内存泄漏

六、总结

本文从基础的纯函数与副作用出发,讲解了 React 中最核心的两个 Hooks,核心知识点总结如下:

  1. 纯函数 :相同输入返回相同输出,无副作用、同步执行;副作用:修改外部变量、请求、定时器等对外部环境的操作
  2. useState :为函数组件添加响应式状态,支持函数式初始化 (复杂计算)和函数式更新(依赖上一次状态)
  3. useEffect :处理所有副作用,通过依赖项数组 控制执行时机,返回清理函数清除副作用,避免内存泄漏
  4. Hooks 通用规则:仅在函数组件 / 自定义 Hooks 的顶层调用
  5. 异步请求初始化数据:使用useEffect空依赖实现,而非useState的初始化函数

useStateuseEffect是 React Hooks 的基础,掌握这两个 Hooks,就能实现大部分函数组件的开发需求。后续可以继续学习useRefuseContextuseReducer等进阶 Hooks,以及自定义 Hooks 的封装,让 React 代码更简洁、更高效。

最后:建议大家跟着本文的案例手动敲一遍代码,体会状态更新和副作用执行的时机,只有实战才能真正掌握 Hooks 的核心逻辑!

相关推荐
光影少年13 小时前
react的Context 跨层传值、优缺点、适用场景
前端·react.js·掘金·金石计划
JiaWen技术圈16 小时前
React Server Functions 深度解析
前端·react.js·前端框架
JiaWen技术圈17 小时前
React 19 并发渲染器:全面解析与实战指南
前端·react.js·前端框架
Ruihong18 小时前
VuReact v1.8.4 发布:Vue 迁移 React 编译器迎来稳定性大修,这些坑终于被填平了
前端·vue.js·react.js
从文处安18 小时前
「React Router v7 教程」从零到全栈,一篇搞定
前端·react.js
卸任19 小时前
打造基于 Milkdown 的所见即所得 Markdown 编辑器
前端·react.js·markdown
JiaWen技术圈19 小时前
React 19 Fiber 架构 深度解析
前端·react.js·架构
暗冰ཏོ19 小时前
《Vue + React + Java + PHP 项目部署到服务器完整指南》
java·服务器·vue.js·react.js·项目部署
JeariCk19 小时前
React Compiler 1.0 发布半年后的现状
react.js
. . . . .20 小时前
React Native
react native·react.js