Next.js中useEffect多次执行的原因排查
问题:Next.js 13.4.16中TimeTracking组件的useEffect为何多次执行?
问题描述
在Next.js 13.4.16版本开发TimeTracking客户端组件时,组件内的useEffect依赖session,但控制台打印显示该钩子多次执行。以下是相关代码及控制台截图,求分析原因。
相关代码
TimeTracking组件代码
'use client'; import React, { useEffect, useState } from 'react'; import { useSession } from 'next-auth/react'; const TimeTracking = () => { const { data: session } = useSession(); useEffect(() => { console.log('session ===> ', session); let initialTimeStampSetUp: boolean = false; console.log('initialTimeStampSetUp ===> ', initialTimeStampSetUp); if (!initialTimeStampSetUp) { const initialTimeStamp: Date = new Date(); console.log('initialTimeStamp ===> ', initialTimeStamp); initialTimeStampSetUp = true; console.log('initialTimeStampSetUp ===> ', initialTimeStampSetUp); } if (typeof window !== 'undefined' && session) { const handleBeforeUnload = () => { const requestBody = JSON.stringify({ initialTimeStamp: initialTimeStamp, date: new Date(), }); navigator.sendBeacon(`/api/user/${session?.user?._id}`, requestBody); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; } }, [session]); return ( <> <p>Time Tracking</p> </> ); }; export default TimeTracking;
layout.tsx代码
import '../scss/style.scss'; import './globals.css'; import { Inter } from 'next/font/google'; import Provider from '@/app/components/Provider'; import React, { ReactNode } from 'react'; import type { Metadata } from 'next'; const inter = Inter({ subsets: ['latin'] }); import TimeTracking from '@/app/components/TimeTracking'; export const metadata: Metadata = { title: 'LangAI app', description: 'Adaptive language learning app', }; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <head> <link rel="manifest" href="/manifest.json" /> <link rel="apple-touch-icon" href="/icon.png"></link> <meta name="theme-color" content="#fff" /> </head> <body className={inter.className} suppressHydrationWarning={true}> <div className="min-h-screen"> <Provider> {children} <TimeTracking /> </Provider> </div> </body> </html> ); }
控制台截图

原因分析
- App Router的客户端组件渲染机制:Next.js 13 App Router中,客户端组件会经历服务端水合(hydration)和客户端挂载两个阶段,这两个阶段都会触发useEffect执行,属于框架默认行为。
- session状态的多阶段变化:
useSession()返回的session状态会经历至少两个关键阶段:初始加载时为null(未完成认证检测),认证请求完成后返回用户session数据(非null);如果后续存在token刷新等操作,session还会再次更新。每次session状态改变,都会触发依赖它的useEffect重新执行。 - 代码内部的变量作用域问题:你在useEffect内部声明的
initialTimeStampSetUp是局部变量,每次useEffect执行都会重新初始化,导致这个标记完全起不到“只执行一次初始化”的作用,反而会让你误以为钩子执行了多次。
解决方案
- 标记初始化状态,控制仅执行一次核心逻辑:使用
useState存储初始化状态,避免每次useEffect执行时重复初始化:
'use client'; import React, { useEffect, useState } from 'react'; import { useSession } from 'next-auth/react'; const TimeTracking = () => { const { data: session } = useSession(); const [isInitialized, setIsInitialized] = useState(false); const [initialTimeStamp, setInitialTimeStamp] = useState<Date | null>(null); useEffect(() => { if (typeof window === 'undefined' || !session || isInitialized) return; // 仅执行一次的初始化逻辑 const timestamp = new Date(); setInitialTimeStamp(timestamp); setIsInitialized(true); const handleBeforeUnload = () => { if (!initialTimeStamp) return; const requestBody = JSON.stringify({ initialTimeStamp, date: new Date(), }); navigator.sendBeacon(`/api/user/${session.user._id}`, requestBody); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [session, isInitialized, initialTimeStamp]); return ( <> <p>Time Tracking</p> </> ); }; export default TimeTracking;
- 稳定useEffect依赖项:如果session对象频繁更新但核心数据(如用户ID)不变,可以只依赖
session?.user?._id,减少不必要的钩子触发:
useEffect(() => { // ...逻辑 }, [session?.user?._id]);
内容的提问来源于stack exchange,提问作者in43sh
相关产品推荐
相关产品推荐

