You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
  );
}

控制台截图

控制台截图


原因分析

  1. App Router的客户端组件渲染机制:Next.js 13 App Router中,客户端组件会经历服务端水合(hydration)和客户端挂载两个阶段,这两个阶段都会触发useEffect执行,属于框架默认行为。
  2. session状态的多阶段变化:useSession()返回的session状态会经历至少两个关键阶段:初始加载时为null(未完成认证检测),认证请求完成后返回用户session数据(非null);如果后续存在token刷新等操作,session还会再次更新。每次session状态改变,都会触发依赖它的useEffect重新执行。
  3. 代码内部的变量作用域问题:你在useEffect内部声明的initialTimeStampSetUp是局部变量,每次useEffect执行都会重新初始化,导致这个标记完全起不到“只执行一次初始化”的作用,反而会让你误以为钩子执行了多次。

解决方案

  1. 标记初始化状态,控制仅执行一次核心逻辑:使用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;
  1. 稳定useEffect依赖项:如果session对象频繁更新但核心数据(如用户ID)不变,可以只依赖session?.user?._id,减少不必要的钩子触发:
useEffect(() => {
  // ...逻辑
}, [session?.user?._id]);

内容的提问来源于stack exchange,提问作者in43sh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 03:16:14