3、useEffect 与副作用管理

3 阅读4分钟

太棒了!👏 你已经掌握了组件如何“说话”,现在是时候让它们“对外联系”了!


—— 让 React 连接外部世界

⚠️ 这是 React 中最强大也最容易被误解的 Hook。
掌握它,你就能:

  • 从 API 获取数据
  • 设置定时器
  • 监听键盘/窗口事件
  • 操作 DOM
  • 清理资源(防内存泄漏)

一、什么是“副作用”?🧬

在 React 中,副作用(Side Effect) 是指那些不在渲染过程中发生,但会影响应用的行为,比如:

常见副作用示例
数据请求fetch('/api/user')
订阅/监听window.addEventListener
手动修改 DOMref.current.focus()
设置定时器setInterval
日志打印console.log(开发用)

✅ 简单说:
渲染函数(组件)应该是“纯”的 —— 输入 props/state → 输出 JSX。
所有“会改变外部世界”的操作,都应放在 useEffect 中。


二、useEffect 基本语法 🧩

useEffect(() => {
  // 副作用逻辑(如:请求数据、添加监听)
  
  return () => {
    // 清理函数(可选)—— 组件卸载时执行
    // 如:清除定时器、取消订阅、断开连接
  };
}, [依赖项数组]); // 控制执行时机

三、三种使用模式(必掌握)✅

🔹 模式 1:仅在组件挂载时执行一次

👉 相当于类组件的 componentDidMount

useEffect(() => {
  console.log('组件已挂载');
  // 常用于:首次加载数据
  fetchUserData();
}, []); // 依赖数组为空 → 只执行一次

✅ 适用场景:

  • 获取初始数据(用户信息、文章列表)
  • 设置一次性监听器
  • 发送埋点日志

🔹 模式 2:在特定数据变化时执行

👉 相当于 componentDidUpdate

useEffect(() => {
  console.log('用户名变了,重新加载数据');
  fetch(`/api/user/${userId}`);
}, [userId]); // 当 userId 改变时重新执行

✅ 适用场景:

  • 根据路由参数加载不同内容
  • 搜索关键词变化时发起请求
  • 表单字段变化时验证或预览

🔹 模式 3:每次渲染后都执行(慎用)

👉 没有依赖数组

useEffect(() => {
  document.title = `当前用户:${name}`;
  // 每次 name 变化都会执行
});
// 没写依赖数组 → 每次渲染后都执行

⚠️ 警告:容易导致无限循环或性能问题,建议明确写出依赖项。


四、实战案例 💡

🌐 案例 1:从 API 获取用户数据

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        console.error('加载失败', err);
        setLoading(false);
      });
  }, [userId]); // 用户ID变化时重新请求

  if (loading) return <p>加载中...</p>;
  if (!user) return <p>用户不存在</p>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

⏱️ 案例 2:构建一个倒计时组件

function Countdown({ seconds }) {
  const [timeLeft, setTimeLeft] = useState(seconds);

  useEffect(() => {
    // 设置定时器
    const timer = setInterval(() => {
      setTimeLeft(prev => {
        if (prev <= 1) {
          clearInterval(timer); // 清除定时器
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    // 清理函数:组件卸载时清除定时器
    return () => clearInterval(timer);
  }, []); // 只在挂载时设置一次

  return <div>剩余时间:{timeLeft} 秒</div>;
}

✅ 关键点:

  • return () => clearInterval(timer) 防止内存泄漏
  • 即使组件被销毁,定时器也会继续运行(除非清理)

🎧 案例 3:监听键盘事件(全局快捷键)

function App() {
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Escape') {
        alert('你按了 Esc 键!');
      }
    };

    window.addEventListener('keydown', handleKeyDown);

    // 清理:移除监听器
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []); // 只绑定一次

  return <div>按 Esc 键试试</div>;
}

五、常见陷阱与最佳实践 ⚠️✅

❌ 错误 1:忘记依赖项 → 闭包陷阱

// ❌ 错误:count 是旧值,永远是 0
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count); // 始终是初始值
  }, 1000);
}, []); // 没依赖 count → 拿不到最新值

✅ 正确做法:

// 方案 1:加依赖 → 每次 count 变化重建定时器
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);

// 方案 2:用函数式更新 + ref(高级)
// 或使用 useInterval 自定义 Hook

❌ 错误 2:异步函数直接作为 useEffect 回调

// ❌ 错误写法
useEffect(async () => {
  const res = await fetch('/api/data');
  setData(await res.json());
}, []);

✅ 正确做法:在内部定义异步函数

useEffect(() => {
  const loadData = async () => {
    try {
      const res = await fetch('/api/data');
      setData(await res.json());
    } catch (err) {
      console.error(err);
    }
  };

  loadData();
}, []);

✅ 最佳实践总结

建议说明
明确写出依赖项避免闭包陷阱,让 React 精确控制执行时机
清理副作用定时器、事件监听、订阅必须清理
拆分逻辑不同目的的副作用分开写多个 useEffect
使用 ESLint 插件eslint-plugin-react-hooks 可自动检测依赖遗漏

六、实战练习 🏋️‍♀️

✅ 练习 1:天气组件

  • 组件接收 city prop
  • 使用 useEffectcity 改变时请求天气 API(可用 mock 数据)
  • 显示温度和天气描述
// mock 请求
const fetchWeather = (city) => 
  Promise.resolve({ temp: 25, desc: '晴' });

✅ 练习 2:页面标题更新器

  • 创建一个 PageTitle 组件
  • 接收 title prop
  • 使用 useEffect 动态设置 document.title
  • 组件卸载时恢复为默认标题(如 "我的网站")

✅ 练习 3:窗口大小监听器

  • 使用 useEffect 监听 window.resize
  • 保存当前窗口宽度到 state
  • 显示 “当前宽度:xxx px”
  • 卸载时移除监听器

✅ 总结:useEffect 核心要点

概念要点
执行时机渲染完成后(异步)
依赖数组 []控制何时重新执行
清理函数防止内存泄漏,必须写
常见用途数据请求、事件监听、定时器、DOM 操作
不能直接 async需在内部调用异步函数
避免无限循环检查依赖项是否导致重复执行

🎯 下一步预告
你已经能让组件“对外交互”了!接下来我们要更进一步:

➡️ 自定义 Hook:封装可复用逻辑
—— 把 useEffect + useState 组合起来,写出自己的 Hook,比如 useFetch, useLocalStorage

是否继续?我将带你进入 第五课:自定义 Hook 的艺术