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

4 阅读12分钟

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

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

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

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

1.1 纯函数

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

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

纯函数示例

// 纯函数:输入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
  • 订阅 / 取消订阅事件

副作用示例

// 有副作用:修改了外部的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 基本使用

语法

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

基础示例

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:函数式初始化

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

语法

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

实战示例

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 状态更新的异步性导致的取值错误。

语法

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

实战示例

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 基本语法

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

3.3 三种使用场景(核心)

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

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

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

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

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

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

实战示例

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(空依赖)实现,这是项目中的高频用法。

实战示例

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

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

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 的核心逻辑!