一文带你了解React useEffect:核心概念与实践总结

2 阅读7分钟

前言

大家好,本文将详细介绍react中的重要知识点-- useEffect 的核心概念、使用场景和最佳实践,特别适合刚接触 React Hooks 的开发者。


React Hooks 编程基础

在深入 useEffect 之前,让我们先了解一些 React Hooks 的基础知识,下面从大家可能最熟悉的useState开始说起。

useState 简介

useState 是 React 最基础的 Hook,用于在函数组件中添加状态管理。它接收一个初始值,返回一个包含当前状态和更新函数的数组,它让函数组件拥有了状态管理的能力:

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

以 use 开头的函数:这是 React Hooks 的命名约定,表明这是一个 Hook

在react中,有以下常见hook:

  1. useState
    用于在函数组件中添加状态管理
    const [state, setState] = useState(initialState);
  2. useEffect
    处理副作用操作(数据获取、订阅、手动DOM操作等)
    useEffect(() => { /* effect */ }, [dependencies]);
  3. useContext
    接收一个 context 对象并返回该 context 的当前值
    const value = useContext(MyContext);
  4. useReducer
    更复杂的状态逻辑管理,类似于 Redux 的 reducer
    const [state, dispatch] = useReducer(reducer, initialState);
  5. useCallback
    缓存回调函数,避免不必要的重新创建
    const memoizedCallback = useCallback(() => { /* 函数 */ }, [deps]);

useEffect是什么?

image.png

useEffect 是 React 中最核心的 Hook 之一,用于在函数组件中执行副作用操作。它相当于类组件中的 componentDidMountcomponentDidUpdate 和 componentWillUnmount 的结合体,但更加灵活和强大。

什么是副作用?

在编程中,副作用(Side Effect)  指的是函数或表达式在执行时,对外部环境(如全局变量、DOM、网络请求、文件系统等)产生的影响。简单来说,就是除了返回计算结果之外,还做了其他事情


React 中的副作用

在 React 组件中,副作用通常指那些不直接参与 UI 渲染,但会影响外部环境的操作,例如:

  1. 数据获取(API 请求) (如 fetchaxios
  2. 手动修改 DOM(如 document.titlewindow.scrollTo
  3. 订阅事件(如 addEventListenerWebSocket 连接)
  4. 设置定时器(如 setTimeoutsetInterval
  5. 存储数据(如 localStorageIndexedDB

副作用的分类

类型例子是否属于副作用?
纯函数const sum = (a, b) => a + b;❌ 无副作用
副作用操作console.log()fetch()setTimeout()✅ 有副作用
React 状态更新setState()✅ 有副作用(影响组件渲染)

为什么需要 useEffect

React 的核心工作是根据状态(state)渲染 UI,而副作用(如数据请求)可能会影响渲染结果或性能。因此,React 提供了 useEffect,让开发者可以:

  1. 在合适的时机(如组件渲染后)执行副作用,避免阻塞 UI 更新。
  2. 控制副作用的执行频率(通过依赖项数组 [])。
  3. 清理副作用(如取消请求、移除事件监听),防止内存泄漏。

所以总结如下:

  • 副作用 = 影响外部世界的操作(如 API 请求、DOM 操作、定时器等)。
  • useEffect 是 React 管理副作用的 Hook,用于在组件生命周期中安全地执行和清理这些操作。
  • 纯函数(无副作用)是理想情况,但现实开发中,副作用是不可避免的,关键是如何合理控制它们。

如何使用useEffect呢?

 基本语法结构

useEffect(() => {
  // 副作用逻辑代码
  return () => {
    // 清理函数(可选)
  };
}, [dependencies]); // 依赖数组(可选)

四种常见使用模式

1.每次渲染后都执行

useEffect(() => {
  console.log('组件更新后执行');
});
// 注意:没有依赖数组参数

2.仅在挂载时执行一次

useEffect(() => {
  console.log('仅在组件挂载时执行');
  return () => {
    console.log('组件卸载时执行');
  };
}, []); // 空依赖数组

3.特定状态变化时执行

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

useEffect(() => {
  console.log(`count值变为:${count}`);
}, [count]); // 仅在count变化时执行

4.清理副作用示例

useEffect(() => {
  const timer = setInterval(() => {
    console.log('定时器运行中...');
  }, 1000);

  return () => {
    clearInterval(timer); // 组件卸载时清除定时器
  };
}, []);

Effect的一个demo

下面,给出一个完整的例子的代码来说明effect的使用

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

function UseEffectExamples() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState('初始数据');
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  // 模式1: 每次渲染后都执行
  useEffect(() => {
    console.log('【模式1】组件渲染完成或更新后执行');
  }); // 没有依赖数组

  // 模式2: 仅在挂载时执行一次
  useEffect(() => {
    console.log('【模式2】组件挂载时执行 (仅一次)');
    
    return () => {
      console.log('【模式2】组件卸载时执行');
    };
  }, []); // 空依赖数组

  // 模式3: 特定状态变化时执行
  useEffect(() => {
    console.log(`【模式3】count值变化时执行,当前count: ${count}`);
    
    // 这里可以添加count变化时需要执行的逻辑
    document.title = `当前计数: ${count}`;
    
    return () => {
      console.log('【模式3】count即将变化,或组件卸载时执行');
    };
  }, [count]); // 依赖count

  // 模式4: 清理副作用的示例 (窗口大小监听)
  useEffect(() => {
    console.log('【模式4】设置窗口大小监听器');
    
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
      console.log('窗口大小变化:', window.innerWidth);
    };
    
    window.addEventListener('resize', handleResize);
    
    return () => {
      console.log('【模式4】移除窗口大小监听器');
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组,只设置一次

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', margin: '20px' }}>
      <h2>useEffect 四种使用模式示例</h2>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式1: 每次渲染后都执行</h3>
        <p>查看控制台输出</p>
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式2: 仅挂载时执行一次</h3>
        <p>查看控制台初始化和卸载时的输出</p>
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式3: count变化时执行 (当前count: {count})</h3>
        <button onClick={() => setCount(c => c + 1)}>增加count</button>
        <p>查看控制台和页面标题的变化</p>
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>模式4: 清理副作用示例</h3>
        <p>当前窗口宽度: {windowWidth}px</p>
        <p>尝试改变浏览器窗口大小,查看控制台输出</p>
      </div>
      
      <div>
        <h3>数据变化示例</h3>
        <input 
          value={data}
          onChange={(e) => setData(e.target.value)}
          placeholder="输入数据观察模式1的执行"
        />
      </div>
    </div>
  );
}

export default UseEffectExamples;

让我们来对这个demo进行详细地解析:

注意:我们需要先关闭严格模式,打开src中的main.jsx
去除<StrictMode>

  // 去除StrictMode 严格模式
  //<StrictMode>
    <App />
  //</StrictMode>,

首先分析初次渲染时

aaaaa.gif

image.png 当组件首次加载时,所有 useEffect 钩子都会按照声明的顺序依次执行:

  1. 模式1:立即执行,因为没有依赖数组,控制台输出"【模式1】组件渲染完成或更新后执行"

  2. 模式2:立即执行,因为是空依赖数组且首次渲染,控制台输出"【模式2】组件挂载时执行 (仅一次)"

  3. 模式3:立即执行,因为 count 的初始值(0)被视为"变化",控制台输出"【模式3】count值变化时执行,当前count: 0",同时更新文档标题

  4. 模式4:立即执行,因为是空依赖数组且首次渲染:

    • 控制台输出"【模式4】设置窗口大小监听器"
    • 添加窗口 resize 事件监听器

当我们点击增加count时

bbbb.gif

image.png

当用户点击"增加count"按钮时:

  1. 状态更新:setCount 触发状态更新,count 值增加1

  2. 重新渲染:组件使用新的 count 值重新渲染

  3. useEffect 执行

    • 模式1:再次执行(每次渲染都执行),控制台输出

    • 模式2:不执行(依赖数组为空且不是首次渲染)

    • 模式3

      • 先执行上一次 effect 的清理函数(如果有),输出"【模式3】count即将变化,或组件卸载时执行"
      • 然后执行新的 effect,输出"【模式3】count值变化时执行,当前count: [新值]"
      • 更新文档标题
    • 模式4:不执行(依赖数组为空且不是首次渲染)


当我们缩放页面时

ccc.gif

image.png

当用户调整浏览器窗口大小时:

  1. 事件触发:浏览器触发 resize 事件

  2. 事件处理:模式4中注册的 handleResize 函数被执行:

    • 更新 windowWidth 状态
    • 控制台输出"窗口大小变化: [新宽度]"
  3. 状态更新:setWindowWidth 触发状态更新

  4. 重新渲染:组件使用新的 windowWidth 值重新渲染

  5. useEffect 执行

    • 模式1:再次执行(每次渲染都执行),控制台输出
    • 其他模式不执行(因为它们的依赖项没有变化)

组件卸载时的执行情况

虽然示例中没有直接展示卸载过程,但了解组件卸载时的行为很重要:

  1. 执行所有清理函数

    • 模式2:执行清理函数,输出"【模式2】组件卸载时执行"
    • 模式4:执行清理函数,输出"【模式4】移除窗口大小监听器",并实际移除事件监听器
  2. 其他模式

    • 模式1没有清理函数,不执行任何操作
    • 模式3如果count在上次渲染后有变化,也会执行其清理函数

总结

这篇文章较新手向,主要介绍了以下内容:

  1. useEffect 的本质
    React 提供的用于管理副作用的 Hook,统一了类组件中的 componentDidMountcomponentDidUpdate 和 componentWillUnmount 生命周期方法。

  2. 副作用的定义
    任何影响组件外部世界或与外部系统交互的操作,包括数据获取、订阅、定时器、手动 DOM 操作等。

  3. 四种核心模式

    • 每次渲染后执行(无依赖数组)
    • 仅挂载时执行(空依赖数组 []
    • 特定状态变化时执行(指定依赖项 [state]
    • 需要清理的副作用(返回清理函数)