
在 React 应用开发中,useEffect 是处理副作用(如数据获取、订阅、手动修改 DOM 等)的核心 Hook。然而,开发者经常会遇到 useEffect 在某些情况下被执行两次的问题,尤其是在开发环境中。这不仅可能导致不必要的性能开销,还可能引发数据重复提交等严重错误。本文将以一个具体的会话ID生成场景为例,详细分析 useEffect 双重执行的原因及解决方案。
useEffect 在开发模式下双重执行并非 bug,而是 React 严格模式(Strict Mode)的预期行为。
React 严格模式 (Strict Mode) 在开发环境中,React 的严格模式会刻意地双重调用 useEffect 中的 effect 函数(以及一些其他生命周期方法),目的是帮助开发者发现潜在的副作用问题。例如,如果 useEffect 中的副作用没有正确清理,或者其执行不是幂等的,双重调用会暴露这些问题,例如内存泄漏、不当的状态更新或重复的网络请求。严格模式下,组件在挂载后会立即卸载并重新挂载,导致 useEffect 及其清理函数被调用两次,从而触发 effect 函数两次。在生产环境中,useEffect 默认只执行一次。
不恰当的状态管理 当 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)原始代码存在以下几个关键问题:
解决此问题的核心在于:
以下是修正后的代码:
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);修正后的代码改进点:
useEffect(() => {
const timer = setTimeout(() => console.log('Hello'), 1000);
return () => clearTimeout(timer); // 清理函数
}, []);虽然本例中没有直接的清理需求,但这是 useEffect 的重要组成部分。
useEffect 在 React 严格模式下的双重执行是帮助开发者构建更健壮应用的设计选择。解决由此引发的问题,关键在于深入理解 useEffect 的生命周期、严格模式的行为,并采取恰当的状态管理策略。通过本例,我们展示了如何通过细致的状态管理和逻辑重构,有效地解决 useEffect 重复触发数据库操作的问题,提升应用的健壮性和用户体验。核心在于确保副作用的幂等性,并使状态更新与副作用的执行逻辑保持同步和清晰。
以上就是解决 React useEffect 双重执行与状态管理陷阱:以会话ID生成为例的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号