0

0

解决 React useEffect 双重执行与状态管理陷阱:以会话ID生成为例

霞舞

霞舞

发布时间:2025-10-11 11:26:45

|

331人浏览过

|

来源于php中文网

原创

解决 react useeffect 双重执行与状态管理陷阱:以会话id生成为例

本文深入探讨 React useEffect 在开发模式下双重执行的常见原因,特别是结合 Next.js 和 tRPC 项目中因不当状态管理导致副作用重复触发的问题。通过分析一个会话ID生成场景,我们将演示如何优化 loading 状态初始化、重构 useEffect 逻辑,并提供一个健壮的解决方案,以确保副作用的正确执行和避免资源浪费。

在 React 应用开发中,useEffect 是处理副作用(如数据获取、订阅、手动修改 DOM 等)的核心 Hook。然而,开发者经常会遇到 useEffect 在某些情况下被执行两次的问题,尤其是在开发环境中。这不仅可能导致不必要的性能开销,还可能引发数据重复提交等严重错误。本文将以一个具体的会话ID生成场景为例,详细分析 useEffect 双重执行的原因及解决方案。

useEffect 双重执行的常见原因

useEffect 在开发模式下双重执行并非 bug,而是 React 严格模式(Strict Mode)的预期行为。

  1. React 严格模式 (Strict Mode) 在开发环境中,React 的严格模式会刻意地双重调用 useEffect 中的 effect 函数(以及一些其他生命周期方法),目的是帮助开发者发现潜在的副作用问题。例如,如果 useEffect 中的副作用没有正确清理,或者其执行不是幂等的,双重调用会暴露这些问题,例如内存泄漏、不当的状态更新或重复的网络请求。严格模式下,组件在挂载后会立即卸载并重新挂载,导致 useEffect 及其清理函数被调用两次,从而触发 effect 函数两次。在生产环境中,useEffect 默认只执行一次。

  2. 不恰当的状态管理 当 useEffect 内部的状态更新逻辑与外部条件判断交织时,可能导致即使在 Strict Mode 下,实际的副作用(如网络请求)也被多次触发。如果状态更新没有及时反映到组件的下一次渲染中,或者 useEffect 的依赖项数组设置不当,都可能加剧问题。

问题分析:原始代码的不足

考虑以下 Next.js 应用的 _app.tsx 文件中的 useEffect 逻辑,其目标是检查用户会话ID,如果不存在则生成一个新的会话ID并存储在 Cookie 中。

import { type AppType } from 'next/app'
import { api } from '~/utils/api'
import '~/styles/globals.css'
import Nav from '~/components/Nav'
import { useEffect, useState } from 'react'

const MyApp: AppType = ({ Component, pageProps }) => {
  const [showCart, setShowCart] = useState(false)
  const [loading, setLoading] = useState(false) // 问题点1:初始为false

  const createSession = api.user.createSession.useMutation()

  const generateId = async (): Promise => {
    const res = await createSession.mutateAsync()
    if (res.response) {
      return res.response.id
    } else if (res.error) {
      return res.error
    }
  }

  const setSessionId = async () => {
    const tmp: string | undefined = await generateId()
    if (tmp) document.cookie = `sessionId=${tmp}`
    setLoading(false)
  }

  useEffect(() => {
    if (!loading) { // 问题点2:可能在Strict Mode下导致重复执行
      setLoading(true) // 问题点3:可能在useEffect再次执行前未生效
      const cookieString: string = document.cookie
      const cookies: string[] = cookieString.split(';') || []
      let sessionId: string | null = null

      for (let i = 0; i < cookies.length; i++) {
        const cookie: string | undefined = cookies[i]
        if (!cookie || cookie.trim() === '') {
          continue
        }
        if (cookie.trim().startsWith('sessionId=')) {
          sessionId = cookie.trim().substring('sessionId='.length)
          break
        }
      }
      if (!sessionId) {
        void setSessionId() // 异步操作,可能触发数据库写入
      }
    }
  }, []) // 依赖项数组为空

  return (
    <>
      

原始代码存在以下几个关键问题:

  1. loading 状态初始化不当:loading 状态被初始化为 false。在组件首次渲染时,loading 为 false,满足 useEffect 内部的 if (!loading) 条件。
  2. useEffect 内部的条件判断:useEffect 内部使用 if (!loading) 来防止重复执行。然而,在 React 严格模式下,useEffect 会被执行两次。
    • 第一次执行时,loading 为 false,进入条件块,setLoading(true) 被调用。
    • 组件卸载并重新挂载(严格模式行为)。
    • 第二次执行 useEffect 时,由于组件尚未重新渲染以反映 loading 状态的更新,loading 仍然可能为 false(或者在极短的时间内再次变为 false),导致 if (!loading) 再次为真,从而触发 setSessionId(),进而导致 createSession.mutateAsync() 被调用两次,造成数据库重复写入。
  3. 异步操作与状态更新的竞态条件:setSessionId 是一个异步函数,在它完成并调用 setLoading(false) 之前,组件可能已经经历了多次渲染循环,使得 loading 状态的管理变得混乱。
  4. 加载指示器显示不准确:由于 loading 状态的更新逻辑混乱,

    {loading && 'LOADING'}

    可能无法准确反映实际的加载状态。

解决方案:优化 useEffect 及状态管理

解决此问题的核心在于:

美图AI开放平台
美图AI开放平台

美图推出的AI人脸图像处理平台

下载
  1. 正确初始化 loading 状态:当组件首次加载时,我们通常希望显示加载状态,直到所有必要的数据或初始化操作完成。将 loading 初始值设为 true 更符合直观逻辑。
  2. 重构 useEffect 逻辑:确保 useEffect 内部的副作用逻辑是幂等的,并且状态更新与副作用的生命周期保持一致。

以下是修正后的代码:

import { type AppType } from 'next/app';
import { api } from '~/utils/api';
import '~/styles/globals.css';
import Nav from '~/components/Nav';
import { useEffect, useState } from 'react';

const MyApp: AppType = ({ Component, pageProps }) => {
  const [showCart, setShowCart] = useState(false);
  // 优化1: 初始化 loading 状态为 true,表示组件刚开始加载时处于加载中
  const [loading, setLoading] = useState(true); 

  const createSession = api.user.createSession.useMutation();

  const generateId = async (): Promise => {
    const res = await createSession.mutateAsync();
    if (res.response) {
      return res.response.id;
    } else if (res.error) {
      return res.error;
    }
  };

  const setSessionId = async () => {
    const tmp: string | undefined = await generateId();
    if (tmp) document.cookie = `sessionId=${tmp}`;
    // 优化2: 在异步操作完成后设置 loading 为 false
    setLoading(false); 
  };

  useEffect(() => {
    // 优化3: 封装获取 sessionId 的逻辑,提高可读性和复用性
    const getSessionId = () => {
      const cookieString: string = document.cookie;
      const cookies: string[] = cookieString.split(';') || [];
      let sessionId: string | null = null;

      for (let i = 0; i < cookies.length; i++) {
        const cookie: string | undefined = cookies[i];
        if (!cookie || cookie.trim() === '') {
          continue;
        }
        if (cookie.trim().startsWith('sessionId=')) {
          sessionId = cookie.trim().substring('sessionId='.length);
          break;
        }
      }
      return sessionId;
    };

    if (!getSessionId()) { // 优化4: 如果没有 sessionId,则生成一个新的
      void setSessionId();
    } else {
      // 优化5: 如果已经存在 sessionId,则直接结束 loading 状态,无需执行异步操作
      setLoading(false); 
    }
  }, []); // 依赖项数组为空,表示只在组件挂载时执行一次

  return (
    <>
      

修正后的代码改进点:

  1. loading 状态初始化:useState(true) 确保组件在开始渲染时就处于加载状态,直到会话ID检查和生成过程完成。
  2. useEffect 逻辑简化:移除了 if (!loading) 条件,因为 loading 状态现在由 useEffect 内部的逻辑(即 setSessionId 或直接 setLoading(false))来精确控制。
  3. 明确的会话ID检查:getSessionId() 函数清晰地负责检查现有会话ID。
  4. 条件性地执行异步操作:只有当 sessionId 不存在时,才调用 setSessionId() 执行异步操作。
  5. 即时更新 loading 状态:无论是否需要生成新的 sessionId,在 useEffect 的逻辑分支结束时,都会调用 setLoading(false) 来更新 loading 状态,确保加载指示器在必要时被隐藏。这使得 useEffect 的行为更加确定和幂等。
  6. 正确的加载指示器显示:{loading &&

    LOADING

    } 能够准确地根据 loading 状态显示或隐藏加载文本。

注意事项与最佳实践

  • 理解严格模式:明确 Strict Mode 的作用是为了帮助开发,不应试图“绕过”它的双重执行,而应确保副作用的幂等性或正确处理清理。在生产环境中,useEffect 不会双重执行。
  • 副作用的幂等性:设计 useEffect 中的副作用时,要确保即使被执行多次,其结果也是一致的,不会造成数据重复或不一致。本例中,通过先检查 sessionId 的存在性,避免了重复生成。
  • 清理函数:对于会创建订阅、定时器或需要取消网络请求的副作用,务必提供一个清理函数。例如:
    useEffect(() => {
      const timer = setTimeout(() => console.log('Hello'), 1000);
      return () => clearTimeout(timer); // 清理函数
    }, []);

    虽然本例中没有直接的清理需求,但这是 useEffect 的重要组成部分。

  • 依赖项数组:正确设置 useEffect 的依赖项数组至关重要。空数组 [] 表示只在组件挂载时执行一次。如果依赖项发生变化,useEffect 会重新执行。
  • 状态管理原子性:避免在 useEffect 内部根据组件的当前渲染状态(如 loading)来决定是否执行一次性的初始化逻辑。更好的做法是根据外部条件或更“原子化”的状态来判断。

总结

useEffect 在 React 严格模式下的双重执行是帮助开发者构建更健壮应用的设计选择。解决由此引发的问题,关键在于深入理解 useEffect 的生命周期、严格模式的行为,并采取恰当的状态管理策略。通过本例,我们展示了如何通过细致的状态管理和逻辑重构,有效地解决 useEffect 重复触发数据库操作的问题,提升应用的健壮性和用户体验。核心在于确保副作用的幂等性,并使状态更新与副作用的执行逻辑保持同步和清晰。

相关专题

更多
if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

757

2023.08.22

cookie
cookie

Cookie 是一种在用户计算机上存储小型文本文件的技术,用于在用户与网站进行交互时收集和存储有关用户的信息。当用户访问一个网站时,网站会将一个包含特定信息的 Cookie 文件发送到用户的浏览器,浏览器会将该 Cookie 存储在用户的计算机上。之后,当用户再次访问该网站时,浏览器会向服务器发送 Cookie,服务器可以根据 Cookie 中的信息来识别用户、跟踪用户行为等。

6422

2023.06.30

document.cookie获取不到怎么解决
document.cookie获取不到怎么解决

document.cookie获取不到的解决办法:1、浏览器的隐私设置;2、Same-origin policy;3、HTTPOnly Cookie;4、JavaScript代码错误;5、Cookie不存在或过期等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

345

2023.11.23

阻止所有cookie什么意思
阻止所有cookie什么意思

阻止所有cookie意味着在浏览器中禁止接受和存储网站发送的cookie。阻止所有cookie可能会影响许多网站的使用体验,因为许多网站使用cookie来提供个性化服务、存储用户信息或跟踪用户行为。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

411

2024.02.23

cookie与session的区别
cookie与session的区别

本专题整合了cookie与session的区别和使用方法等相关内容,阅读专题下面的文章了解更详细的内容。

88

2025.08.19

js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

510

2023.06.20

js获取当前时间
js获取当前时间

JS全称JavaScript,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言;它是一种属于网络的高级脚本语言,主要用于Web,常用来为网页添加各式各样的动态功能。js怎么获取当前时间呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

244

2023.07.28

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

258

2023.08.03

Golang 性能分析与pprof调优实战
Golang 性能分析与pprof调优实战

本专题系统讲解 Golang 应用的性能分析与调优方法,重点覆盖 pprof 的使用方式,包括 CPU、内存、阻塞与 goroutine 分析,火焰图解读,常见性能瓶颈定位思路,以及在真实项目中进行针对性优化的实践技巧。通过案例讲解,帮助开发者掌握 用数据驱动的方式持续提升 Go 程序性能与稳定性。

1

2026.01.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Sass 教程
Sass 教程

共14课时 | 0.8万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3万人学习

CSS教程
CSS教程

共754课时 | 22.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号