React 中 useEffect 到底怎么用才不会踩坑?全流程详解 + 实例讲透副作用

在 React 函数组件中,useEffect是处理 "副作用" 的核心工具。无论是数据请求、定时器管理,还是 DOM 操作,都离不开它。

先搞懂:什么是 "副作用"?

在 React 中,"副作用" 指的是组件渲染过程之外的操作------ 这些操作不直接参与 UI 渲染,但会影响组件或应用的状态。常见的副作用包括:

  • 发送网络请求(获取数据)
  • 操作 DOM(如自动聚焦输入框)
  • 设置定时器、订阅事件(如 WebSocket)
  • 手动修改页面标题

组件的主要任务是 "渲染 UI"(正作用),而副作用需要在渲染完成后执行,否则会阻塞渲染,影响页面性能。useEffect的作用就是将副作用与渲染过程分离,确保渲染优先,副作用在渲染完成后再执行。

jsx 复制代码
function App() {
  // 组件的主要任务:渲染UI
  return <div>Hello World</div>;
}

如果直接在组件函数中写副作用(比如网络请求),会导致这些操作在每次渲染时同步执行,可能阻塞 UI 更新。useEffect则能让副作用 "延后" 执行,不影响渲染效率。

声明一个副作用 生命周期时机解析

useEffect的基本语法很简单,包含三个核心部分:

jsx 复制代码
useEffect(() => {
  // 1. 副作用逻辑(如请求数据、设置定时器)
  console.log('执行副作用');

  // 2. 清理函数(可选,用于回收资源)
  return () => {
    console.log('清理副作用');
  };
}, [
  // 3. 依赖项数组(控制副作用何时执行)
]);

(1)副作用逻辑:要执行的操作

这部分是useEffect的核心,放需要执行的副作用代码。比如请求数据:

jsx 复制代码
useEffect(() => {
  // 副作用:请求接口数据
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log('获取到数据:', data);
  };
  fetchData();
});

(2)清理函数:避免内存泄漏

清理函数是useEffect返回的一个函数,用于回收副作用产生的资源,避免组件卸载后资源残留(如定时器未清除、网络请求未取消)。

jsx 复制代码
useEffect(() => {
  // 副作用:设置定时器
  const timer = setInterval(() => {
    console.log('定时器执行中...');
  }, 1000);

  // 清理函数:组件卸载或副作用重新执行前,清除定时器
  return () => {
    clearInterval(timer);
    console.log('定时器已清除');
  };
}, []);

什么时候执行清理函数?

  • 组件卸载时(如页面跳转,组件被移除)
  • 副作用因依赖项变化而重新执行前(先清理旧的副作用,再执行新的)

React 的函数组件不像 class 组件那样有明显的生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount)。
useEffect 就是函数组件中处理这些生命周期的"代理"。 根据依赖项的不同,useEffect 有以下三种生命周期"行为":

模式 依赖项 触发时机 对应生命周期
初始挂载 [] 组件挂载完成后执行一次 componentDidMount
依赖更新 [count] count 更新时执行 componentDidUpdate
每次渲染 不写依赖 每次渲染后都执行 componentDidUpdate(所有更新)

(3)依赖项数组:控制副作用的执行时机

依赖项数组是useEffect最关键的部分,它决定了副作用 "什么时候执行"。

① 无依赖项数组:每次渲染后都执行

jsx 复制代码
useEffect(() => {
  console.log('每次渲染后都执行');
});

这种写法下,副作用会在初始渲染完成后执行 ,且每次组件更新(重新渲染)后都会再次执行。适合需要 "实时响应所有变化" 的场景(如根据最新状态更新 DOM)。

② 空依赖项数组[]:仅在首次渲染后执行

jsx 复制代码
useEffect(() => {
  console.log('仅在首次渲染后执行一次');
  // 常见场景:初始化数据请求、设置一次性事件监听
}, []);

空依赖项表示 "没有依赖任何状态 / 变量",因此副作用只会执行一次(相当于类组件的componentDidMount)。这是最常用的写法之一,尤其适合 "只需要初始化一次" 的操作。

③ 有依赖项的数组:依赖项变化时执行

jsx 复制代码
const [userId, setUserId] = useState(1);

useEffect(() => {
  console.log(`userId变化为${userId},重新执行副作用`);
  // 场景:根据userId重新请求用户数据
  fetch(`https://api.example.com/user/${userId}`);
}, [userId]); // 依赖userId,只有userId变化时才执行

当数组中的依赖项(如userId)发生变化时,副作用会重新执行(相当于类组件的componentDidUpdate)。这能精准控制副作用的执行时机,避免不必要的重复操作。

常见问题

为什么 useEffect 不能直接用 async?

jsx 复制代码
// 错误写法
useEffect(async () => {
  const data = await fetchData(); // 直接用async会报错
}, []);

原因async函数返回的是 Promise 对象,而useEffect要求回调函数返回 "清理函数" 或undefined。Promise 会被 React 视为 "无效的返回值",导致警告。

正确写法:在内部定义 async 函数并调用:

jsx 复制代码
useEffect(() => {
  const fetchData = async () => { // 内部定义async函数
    const data = await fetchData();
  };
  fetchData(); // 执行
}, []);

清理函数的执行时机

清理函数不仅在 "组件卸载时" 执行,还会在 "副作用因依赖项变化而重新执行前" 执行。比如:

jsx 复制代码
const [id, setId] = useState(1);

useEffect(() => {
  console.log(`副作用执行,当前id:${id}`);
  return () => {
    console.log(`清理函数执行,旧id:${id}`);
  };
}, [id]);

id从 1 变为 2 时,执行顺序是:

  1. 先执行旧的清理函数(打印 "旧 id:1")
  2. 再执行新的副作用(打印 "当前 id:2")

这种机制确保每次副作用执行前,旧的资源已被清理,避免冲突。

实战场景:useEffect 的典型用法

数据请求:组件挂载后获取初始数据

jsx 复制代码
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // 定义异步请求函数
    const getUsers = async () => {
      try {
        const response = await fetch('https://api.example.com/users');
        const data = await response.json();
        setUsers(data); // 存储数据到状态
      } catch (err) {
        console.error('请求失败:', err);
      }
    };

    getUsers(); // 执行请求
  }, []); // 空依赖,仅执行一次

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

定时器管理:避免组件卸载后继续执行

jsx 复制代码
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // 设置定时器,每秒更新秒数
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1); // 函数式更新,不依赖当前seconds
    }, 1000);

    // 清理函数:组件卸载时清除定时器
    return () => {
      clearInterval(timer);
    };
  }, []); // 空依赖,定时器只初始化一次

  return <div>已运行:{seconds}秒</div>;
}

如果不清理定时器,当组件被卸载(比如切换页面)后,定时器仍会继续执行,导致内存泄漏。清理函数确保组件卸载时定时器被正确清除。

条件性执行:依赖项变化时重新执行

jsx 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 当userId变化时,重新请求用户信息
    const fetchUser = async () => {
      const response = await fetch(`https://api.example.com/user/${userId}`);
      const data = await response.json();
      setUser(data);
    };

    fetchUser();

    // 清理函数:取消未完成的请求(避免组件卸载后请求完成导致的问题)
    return () => {
      // 实际开发中可使用AbortController取消请求
    };
  }, [userId]); // 依赖userId,userId变化时重新执行

  if (!user) return <div>加载中...</div>;
  return <div>姓名:{user.name}</div>;
}

userId变化(比如从 1 变成 2)时,副作用会重新执行,请求新的用户数据。这种写法确保 UI 能实时响应依赖项的变化。

相关推荐
转转技术团队8 分钟前
从“v我50”到“疯狂星期四”:HTTPS如何用47天寿命的证书挡住中间人
前端
zeqinjie15 分钟前
Flutter 使用 AI Cursor 快速完成一个图表封装【提效】
前端·flutter
真上帝的左手20 分钟前
24. 前端-js框架-Vue
前端·javascript·vue.js
3Katrina30 分钟前
《Stitch的使用指南以及AI新开发模式杂谈》
前端
无羡仙32 分钟前
按下回车后,网页是怎么“跳”出来的?
前端·node.js
喝拿铁写前端33 分钟前
Vue 实战:构建灵活可维护的菜单系统
前端·vue.js·设计模式
ZzMemory36 分钟前
一套通关CSS选择器,玩转元素定位
前端·css·面试
圆心角39 分钟前
小米面挂了
前端·面试
我的小月月40 分钟前
Vue移动端"回到顶部"组件深度解析:拖拽、动画与性能优化实践
前端
拳打南山敬老院40 分钟前
从零构建一个插件系统(六)低代码场景的插件构建思考
javascript·架构