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

NextAuth会话状态在首页持续卡在Loading状态求助

NextAuth会话加载异常问题

问题现象

首次加载首页时,session的status持续卡在loading状态,主导航栏中的加载spinner无限显示,但仅在首页出现此问题。访问其他页面后,会话状态能正常更新;切换回首页时,状态也可正常显示,仅首次进入首页时触发异常。


相关代码与配置

主导航组件main-nav.tsx

import React from "react"
import Link from "next/link"
import { MainNavItem } from "@/types"

import { cn } from "@/lib/utils"
import { UserAccountNav } from "@/components/user-account-nav"

interface MainNavProps {
  items?: MainNavItem[]
  children?: React.ReactNode
}

export function MainNav({ items }: MainNavProps) {

  return (
    <div className="flex h-16 items-center px-2.5 py-4 md:px-4">
      {items?.length ? (
        <nav className="ml-6 mr-auto hidden md:block">
          <ul className="flex space-x-4">
            {items.map((item) => (
              <li
                key={item.title}
                className={cn(
                  "nav-link relative z-10 cursor-pointer py-0.5 font-medium text-muted-foreground transition-all hover:text-primary sm:text-sm"
                )}
              >
                <Link href={item.href}>{item.title}</Link>
              </li>
            ))}
          </ul>
        </nav>
      ) : null}
      <div className="ml-auto flex items-center gap-4">
        <Link
          href="/contact"
          className="text-sm font-medium text-muted-foreground"
        >
          Contact
        </Link>
        <UserAccountNav />
      </div>
    </div>
  )
}

用户账户导航组件user-account-nav.tsx

"use client"

import React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { signOut, useSession } from "next-auth/react"

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Icons } from "@/components/icons"
import { UserAvatar } from "@/components/user-avatar"

export function UserAccountNav() {
  const { data: session, status } = useSession()
  const pathname = usePathname()

  const name = session?.user?.name
  const image = session?.user?.image

  console.log("Auth status: ", status)

  if (status === "loading") {
    return (
      <Icons.loaderCircle className="size-4 animate-spin text-muted-foreground" />
    )
  }

  return (
    <div className="w-max space-x-2">
      {status === "authenticated" ? (
        <DropdownMenu>
          <DropdownMenuTrigger className="flex items-center space-x-1">
            <UserAvatar
              user={{ name: name || "", image: image || "" }}
              className="size-8"
            />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <div className="flex items-center justify-start gap-2 p-2">
              <div className="flex flex-col space-y-1 leading-none">
                {session.user.email && (
                  <p className="w-[200px] truncate text-sm text-muted-foreground">
                    {session.user.email}
                  </p>
                )}
              </div>
            </div>
            <DropdownMenuSeparator />
            {/* Menu items */}
            <DropdownMenuSeparator />
            <DropdownMenuItem
              className="cursor-pointer"
              onSelect={(event) => {
                event.preventDefault()
                signOut({
                  callbackUrl: `${window.location.origin}/login`,
                })
              }}
            >
              <Icons.logOut className="mr-2 size-4 text-muted-foreground" />
              Sign out
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      ) : (
        // Conditionally render the Login button only if the pathname is not '/login'
        pathname !== "/login" && (
          <Link
            href="/login"
            className={cn(buttonVariants({ size: "sm" }), "rounded-full")}
          >
            Log in
          </Link>
        )
      )}
    </div>
  )
}

NextAuth提供者组件next-auth-provider.tsx

"use client"

import * as React from "react"
import { SessionProvider } from "next-auth/react"

type NextAuthProviderProps = {
  children: React.ReactNode
}

export function NextAuthProvider({ children }: NextAuthProviderProps) {
  return <SessionProvider>{children}</SessionProvider>
}

根布局/app/layout.tsx

import "@/styles/globals.css"

import { Inter } from "next/font/google"
import NextTopLoader from "nextjs-toploader"

import { Toaster } from "@/components/ui/toaster"
import { NextAuthProvider } from "@/components/next-auth-provider"
import { ReactQueryProvider } from "@/components/react-query-provider"

const inter = Inter({ subsets: ["latin"] })

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>)

  return (
    <html lang="en" className={`${inter.className} scroll-smooth`}>
      <body>
        <ReactQueryProvider>
          <NextAuthProvider>
            <NextTopLoader showSpinner={false} color="#2563EB" />
            {children}
            <Toaster />
          </NextAuthProvider>
        </ReactQueryProvider>
      </body>
      <Toaster />
    </html>
  )
}

首页代码

import { getAiTools } from "@/lib/get-ai-tools"
import { ToolCard } from "@/components/tool-card"

export default async function Home() {
  const aiTools = await getAiTools()

  return (
    <div>
      <h1 className="mb-2 text-center text-primary">INDIE AI TOOLS</h1>
      <p className="mb-16 text-center">AI Tools by Indie Hackers</p>
      <div className="container mx-auto grid max-w-5xl gap-4 sm:grid-cols-4">
        {aiTools.map((tool) => (
          <ToolCard key={tool.name} tool={tool} />
        ))}
      </div>
    </div>
  )
}

版本信息

  • next: 14.1.3
  • next-auth: ^4.24.5

原因分析

  1. 首页SSR与客户端组件 hydration 不匹配:首页是纯服务端渲染组件,而UserAccountNav是客户端组件,服务端渲染时无会话信息,客户端初始化useSession时,因服务端渲染内容与客户端 hydration 状态不一致,导致会话状态持续挂起。
  2. SessionProvider 初始化无服务端会话数据:根布局中SessionProvider未传入服务端预取的会话信息,客户端首次加载时需要从头请求会话,首页SSR的快速渲染导致客户端会话请求被阻塞。
  3. 客户端会话请求优先级问题:首页SSR的大量数据请求(如getAiTools)可能抢占了会话请求的资源,导致会话状态迟迟无法更新。

解决办法

方案1:服务端预取会话并传递给SessionProvider

修改根布局,在服务端获取会话信息,传递给SessionProvider,避免客户端重复请求:

  1. 更新next-auth-provider.tsx接收会话参数:
"use client"

import * as React from "react"
import { SessionProvider } from "next-auth/react"
import { Session } from "next-auth"

type NextAuthProviderProps = {
  children: React.ReactNode
  session?: Session | null
}

export function NextAuthProvider({ children, session }: NextAuthProviderProps) {
  return <SessionProvider session={session}>{children}</SessionProvider>
}
  1. 修改根布局layout.tsx,服务端获取会话:
import "@/styles/globals.css"
import { Inter } from "next/font/google"
import NextTopLoader from "nextjs-toploader"
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth" // 你的NextAuth配置文件

import { Toaster } from "@/components/ui/toaster"
import { NextAuthProvider } from "@/components/next-auth-provider"
import { ReactQueryProvider } from "@/components/react-query-provider"

const inter = Inter({ subsets: ["latin"] })

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  // 服务端预取会话
  const session = await getServerSession(authOptions)

  return (
    <html lang="en" className={`${inter.className} scroll-smooth`}>
      <body>
        <ReactQueryProvider>
          <NextAuthProvider session={session}>
            <NextTopLoader showSpinner={false} color="#2563EB" />
            {children}
            <Toaster />
          </NextAuthProvider>
        </ReactQueryProvider>
      </body>
      <Toaster />
    </html>
  )
}

方案2:给首页添加客户端包裹层

将首页内容拆分为SSR内容和客户端包裹层,确保会话状态加载完成后再渲染页面:

  1. 创建home-content.tsx(保持SSR):
import { getAiTools } from "@/lib/get-ai-tools"
import { ToolCard } from "@/components/tool-card"

export default async function HomeContent() {
  const aiTools = await getAiTools()

  return (
    <div>
      <h1 className="mb-2 text-center text-primary">INDIE AI TOOLS</h1>
      <p className="mb-16 text-center">AI Tools by Indie Hackers</p>
      <div className="container mx-auto grid max-w-5xl gap-4 sm:grid-cols-4">
        {aiTools.map((tool) => (
          <ToolCard key={tool.name} tool={tool} />
        ))}
      </div>
    </div>
  )
}
  1. 创建home-client-wrapper.tsx(客户端组件):
"use client"

import { useSession } from "next-auth/react"
import { useEffect, useState } from "react"
import HomeContent from "./home-content"

export default function HomeClientWrapper() {
  const { status } = useSession()
  const [isReady, setIsReady] = useState(false)

  useEffect(() => {
    if (status !== "loading") {
      setIsReady(true)
    }
  }, [status])

  // 等待会话状态加载完成再渲染内容
  if (!isReady) {
    return null // 可替换为全局加载占位符
  }

  return <HomeContent />
}
  1. 修改首页page.tsx:
import HomeClientWrapper from "./home-client-wrapper"

export default function Home() {
  return <HomeClientWrapper />
}

方案3:优化UserAccountNav的超时处理

在客户端组件中添加超时逻辑,避免无限加载:

"use client"

import React, { useEffect, useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { signOut, useSession } from "next-auth/react"

// ... 保留原有导入代码

export function UserAccountNav() {
  const { data: session, status } = useSession()
  const pathname = usePathname()
  const [isTimedOut, setIsTimedOut] = useState(false)

  // 5秒后若仍处于加载状态,视为超时
  useEffect(() => {
    const timer = setTimeout(() => {
      if (status === "loading") {
        setIsTimedOut(true)
      }
    }, 5000)

    return () => clearTimeout(timer)
  }, [status])

  const name = session?.user?.name
  const image = session?.user?.image

  console.log("Auth status: ", status)

  if (status === "loading" && !isTimedOut) {
    return (
      <Icons.loaderCircle className="size-4 animate-spin text-muted-foreground" />
    )
  }

  // 超时后默认显示未登录状态
  if (isTimedOut) {
    return pathname !== "/login" && (
      <Link
        href="/login"
        className={cn(buttonVariants({ size: "sm" }), "rounded-full")}
      >
        Log in
      </Link>
    )
  }

  // ... 保留原有渲染逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 07:55:09