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
原因分析
- 首页SSR与客户端组件 hydration 不匹配:首页是纯服务端渲染组件,而
UserAccountNav是客户端组件,服务端渲染时无会话信息,客户端初始化useSession时,因服务端渲染内容与客户端 hydration 状态不一致,导致会话状态持续挂起。 - SessionProvider 初始化无服务端会话数据:根布局中
SessionProvider未传入服务端预取的会话信息,客户端首次加载时需要从头请求会话,首页SSR的快速渲染导致客户端会话请求被阻塞。 - 客户端会话请求优先级问题:首页SSR的大量数据请求(如
getAiTools)可能抢占了会话请求的资源,导致会话状态迟迟无法更新。
解决办法
方案1:服务端预取会话并传递给SessionProvider
修改根布局,在服务端获取会话信息,传递给SessionProvider,避免客户端重复请求:
- 更新
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> }
- 修改根布局
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内容和客户端包裹层,确保会话状态加载完成后再渲染页面:
- 创建
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> ) }
- 创建
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 /> }
- 修改首页
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
相关产品推荐
相关产品推荐

