首页 > web前端 > js教程 > 正文

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

霞舞
发布: 2025-10-11 11:26:45
原创
304人浏览过

解决 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<string | undefined> => {
    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 (
    <>
      <Nav showCart={showCart} setShowCart={setShowCart} />
      <h1>{loading && 'LOADING'}</h1> {/* 问题点4:可能无法正确显示 */}
      <Component {...pageProps} />
    </>
  )
}

export default api.withTRPC(MyApp)
登录后复制

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

  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 状态的更新逻辑混乱,<h1>{loading && 'LOADING'}</h1> 可能无法准确反映实际的加载状态。

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

解决此问题的核心在于:

降重鸟
降重鸟

要想效果好,就用降重鸟。AI改写智能降低AIGC率和重复率。

降重鸟 113
查看详情 降重鸟
  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<string | undefined> => {
    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 (
    <>
      <Nav showCart={showCart} setShowCart={setShowCart} />
      {/* 优化6: 只有在 loading 为 true 时才显示 LOADING 文本 */}
      {loading && <h1>LOADING</h1>} 
      <Component {...pageProps} />
    </>
  );
};

export default api.withTRPC(MyApp);
登录后复制

修正后的代码改进点:

  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 && <h1>LOADING</h1>} 能够准确地根据 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 重复触发数据库操作的问题,提升应用的健壮性和用户体验。核心在于确保副作用的幂等性,并使状态更新与副作用的执行逻辑保持同步和清晰。

以上就是解决 React useEffect 双重执行与状态管理陷阱:以会话ID生成为例的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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