React Hooks 完全指南:useState 与 useEffect 核心用法

小飞兽 React 123 次阅读 2026-07-17

React Hooks 完全指南:useState 与 useEffect 核心用法

一、Introduction

React Hooks 是 React 16.8 引入的核心特性,它让你在函数组件中使用 state 和生命周期方法,彻底改变了 React 的开发范式。在此之前,只有 class 组件才能使用 state 和生命周期函数,而 Hooks 的出现让函数组件拥有了完整的能力,使得代码更简洁、更易于复用和测试。

本文重点讲解 useState(状态管理)和 useEffect(副作用处理)这两个最基础也最常用的 Hook,涵盖它们的完整用法、常见陷阱以及实际开发中的最佳实践。

---

二、useState 基础语法

#### 2.1 基本用法

import React, { useState } from 'react';

function Counter() {
  // 声明一个名为 count 的 state 变量,初始值为 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数:{count}</p>
      {/* 点击按钮调用 setCount 更新状态,组件自动重新渲染 */}
      <button onClick={() => setCount(count + 1)}>加 1</button>
      <button onClick={() => setCount(count - 1)}>减 1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

export default Counter;

#### 2.2 useState 的独家特性:函数式更新

当新状态依赖于旧状态时,推荐使用函数式更新,避免闭包陷阱:

function CounterFunctional() {
  const [count, setCount] = useState(0);

  // 错误写法:依赖外部变量,在快速点击时可能漏掉更新
  const handleIncrementWrong = () => {
    setCount(count + 1); // count 是闭包中的旧值
    setCount(count + 1); // 两次调用用的是同一个旧值
    // 结果:只增加了 1,而不是 2
  };

  // 正确写法:使用函数式更新,基于最新状态计算
  const handleIncrementCorrect = () => {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    // 结果:正确增加 2
  };

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={handleIncrementWrong}>错误方式(只+1)</button>
      <button onClick={handleIncrementCorrect}>正确方式(+2)</button>
    </div>
  );
}

#### 2.3 使用对象或数组作为状态

function UserProfile() {
  // 状态可以是任何类型:对象
  const [user, setUser] = useState({
    name: '张三',
    age: 28,
    email: 'zhangsan@example.com'
  });

  // 更新对象状态时,需要展开旧对象,再覆盖要修改的字段
  const updateAge = (newAge) => {
    setUser(prevUser => ({
      ...prevUser,
      age: newAge
    }));
  };

  // 注意:不能直接修改状态,必须通过 setState 创建新对象
  const updateName = (newName) => {
    setUser({ ...user, name: newName }); // 先展开,再用新值覆盖
  };

  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <p>邮箱:{user.email}</p>
      <button onClick={() => updateAge(user.age + 1)}>生日+1岁</button>
      <button onClick={() => updateName('李四')}>改名为李四</button>
    </div>
  );
}

---

三、useEffect 基础语法

#### 3.1 什么是副作用(Side Effect)

副作用是指那些影响组件外部世界的操作,比如:发送 HTTP 请求、操作 DOM、订阅事件、设置定时器等。useEffect 就是用来在组件中处理这些副作用的。

#### 3.2 基本用法

import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // useEffect 接收两个参数:
  // 第一个是副作用处理函数(回调)
  // 第二个是依赖数组(可选),控制何时重新执行
  useEffect(() => {
    // 这个函数在组件挂载后执行一次(类似 componentDidMount)
    console.log('组件已挂载,开始获取数据');

    // 使用 AbortController 在组件卸载时取消请求,避免内存泄漏
    const controller = new AbortController();

    fetch('https://jsonplaceholder.typicode.com/posts/1', {
      signal: controller.signal
    })
      .then(response => {
        if (!response.ok) throw new Error('网络错误');
        return response.json();
      })
      .then(jsonData => {
        setData(jsonData);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setLoading(false);
        }
      });

    // 清理函数:组件卸载或依赖变化时执行(类似 componentWillUnmount)
    return () => {
      console.log('清理:取消未完成的请求');
      controller.abort();
    };
  }, []); // 空依赖数组:只在挂载时执行一次

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误:{error}</div>;

  return (
    <div>
      <h2>{data.title}</h2>
      <p>{data.body}</p>
    </div>
  );
}

---

四、运行效果说明

以上 Counter 组件渲染后:

  • 页面显示"当前计数:0"和三个按钮

  • 点击"加 1",count 从 0 变为 1,组件重新渲染,数字更新为 1

  • 快速连续点击时,函数式更新确保每次都基于最新值计算

  • 对象/数组状态更新时,展开运算符确保不可变性,其他字段保留

DataFetcher 组件:

  • 挂载时自动发起 HTTP 请求

  • loading 状态显示"加载中..."

  • 请求成功后显示文章标题和正文

  • 组件卸载时自动取消 pending 请求,防止 React 警告

---

五、常见问题(FAQ)

#### Q1:useState 的初始值会不会每次渲染都执行?

不会。 useState 的初始值只用在第一次渲染时(类似构造函数),后续渲染即使传入新的初始值,也会被忽略,除非你显式调用 setState 触发更新。

// 这个 console.log 只在首次渲染时打印一次
const [count, setCount] = useState(() => {
  console.log('计算初始值'); // 只执行一次
  return 0;
});

如果你有昂贵的计算逻辑,可以传入一个函数(lazy initial state),这样只在首次渲染时执行一次计算。

#### Q2:useEffect 的依赖数组应该怎么填?

原则:所有在 effect 中使用的变量,都应加入依赖数组。

// 错误:userId 变了但 effect 没有重新执行
useEffect(() => {
  fetchUser(userId); // userId 是外部变量
}, []); // 缺少 userId 依赖

// 正确:把 userId 加入依赖数组
useEffect(() => {
  fetchUser(userId);
}, [userId]); // userId 变化时重新执行

如果确实不需要重新执行,传空数组 []。但要确保 effect 内部的代码不会依赖任何会变化的变量。

#### Q3:为什么应该在 useEffect 中返回一个清理函数?

清理函数防止两类问题:内存泄漏竞态条件

useEffect(() => {
  const timer = setInterval(() => {
    setSeconds(s => s + 1);
  }, 1000);

  // 如果没有这个清理函数,组件卸载后 timer 仍在运行
  // 导致:Warning: Can't perform a React state update on an unmounted component
  return () => clearInterval(timer);
}, []);

---

六、延伸阅读

  • [React useRef 完全指南:操作 DOM 与持久化变量]()
  • [React useCallback 与 useMemo 性能优化指南]()
  • [React 自定义 Hooks 实战:逻辑复用与模块化]()
  • [React Context API 状态管理深度理解]()