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

NextJS App Router中NextAuth验证后Navbar状态不同步问题求助

NextAuth注册后客户端Navbar状态不同步问题

背景

基于NextJS App Router开发,使用NextAuth + Resend实现注册流程,采用NextAuth官方Resend Provider。注册流程:

  • 通过表单获取用户邮箱
  • 提交后调用Server Action触发NextAuth的signIn()
  • 发送自定义6位OTP验证邮件,同时NextAuth Prisma Adapter自动在数据库创建verificationToken
  • 用户点击邮件按钮跳转到验证码输入页
  • 提交验证码后Server Action验证通过,重定向到onboarding页面

问题现象

完成第5步重定向到onboarding页面后:

  • Cookie已生成session token,根layout.tsx中await auth()能获取到session对象
  • Navbar组件中console.log("Session in Navbar: ", session)有输出,但内部isLoggedIn状态未设为true
  • 服务器端日志显示isLoggedIn=true,客户端日志显示isLoggedIn=false
  • 手动刷新onboarding页面后,Navbar状态恢复正常

相关代码

根layout.tsx

import "./globals.css";
import { auth } from "../../auth";
import { cn } from "@/lib/utils";
import { Poppins } from "next/font/google";
import { Navbar } from "@/components/Navbar";
import { SessionProvider } from "next-auth/react";
import { Toaster } from "@/components/ui/toaster";

const fontHeading = Poppins({
    subsets: ["latin"],
    display: "swap",
    variable: "--font-heading",
    weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
});

const fontBody = Poppins({
    subsets: ["latin"],
    display: "swap",
    variable: "--font-body",
    weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
});

export default async function RootLayout({
    children,
}: Readonly<{
    children: React.ReactNode;
}>) {
    const session = await auth();
    console.log("Session in Layout: ", session);

    return (
        <html lang="en">
            <body
                className={cn(
                    "antialiased",
                    fontHeading.variable,
                    fontBody.variable
                )}
            >
                <SessionProvider session={session}>
                    <Navbar />
                    <section className="px-[5%]">{children}</section>
                </SessionProvider>
                <Toaster />
            </body>
        </html>
    );
}
"use client";

import Link from "next/link";
import { Sheet, SheetTrigger, SheetContent } from "@/components/ui/sheet";
import { NavLink, Button } from "@/components/ui/button";
import Logo from "./Logo";
import { Menu } from "lucide-react";
import { Separator } from "./ui/separator";
import { useEffect, useState } from "react";
import { signOut, useSession } from "next-auth/react";

export function Navbar() {
    const { data: session, status, update } = useSession();

    const [isSheetOpen, setSheetOpen] = useState(false);
    const [isLoggedIn, setIsLoggedIn] = useState(false);

    const handleCloseSheet = () => {
        setSheetOpen(false);
    };
    useEffect(() => {
        if (status === "authenticated") {
            setIsLoggedIn(true);
            update();
        } else {
            setIsLoggedIn(false);
        }
    }, [status, session]);

    console.log("Session in Navbar: ", session);
    console.log("Current time is: ", new Date().toLocaleTimeString());

    return (
        // 基于isLoggedIn值修改Navbar UI的逻辑
    )
}

已尝试方案

  • 在Navbar中使用useRouter()刷新页面
  • 使用window.location.reload()模拟手动刷新
  • 在Navbar中用useEffect监听状态

解决方案

1. 移除冗余状态,直接从useSession状态推导登录状态

额外维护的isLoggedInstate与useSession的status/session存在同步延迟,直接基于useSession的状态判断即可:

修改Navbar组件:

"use client";

// 保留其他import
import { signOut, useSession } from "next-auth/react";

export function Navbar() {
    const { data: session, status } = useSession();
    // 直接从status推导登录状态,无需额外state
    const isLoggedIn = status === "authenticated";

    const [isSheetOpen, setSheetOpen] = useState(false);

    const handleCloseSheet = () => {
        setSheetOpen(false);
    };

    console.log("Session in Navbar: ", session);
    console.log("Current time is: ", new Date().toLocaleTimeString());

    return (
        // 直接使用isLoggedIn变量渲染UI
    )
}

2. 在onboarding页面强制更新session

重定向到onboarding后,客户端的useSession可能还持有旧状态,在onboarding页面(客户端组件)触发session更新:

"use client";
import { useSession } from "next-auth/react";
import { useEffect } from "react";

export default function Onboarding() {
    const { update } = useSession();

    useEffect(() => {
        // 页面加载时强制拉取最新session
        update();
    }, [update]);

    return <div>Onboarding Content</div>;
}

3. 确保Server Action中认证逻辑正确

验证OTP的Server Action中,需确保signIn调用正确完成用户认证,再执行重定向:

async function verifyOTP(formData: FormData) {
    const email = formData.get('email') as string;
    const otp = formData.get('otp') as string;

    // 自定义OTP验证逻辑
    const verificationToken = await prisma.verificationToken.findFirst({
        where: { email, token: otp },
    });

    if (!verificationToken || verificationToken.expires < new Date()) {
        throw new Error("Invalid or expired OTP");
    }

    // 完成认证,redirect: false避免自动跳转
    await signIn("resend", { email, redirect: false });
    // 删除已使用的验证token
    await prisma.verificationToken.delete({ where: { id: verificationToken.id } });
    // 重定向到onboarding
    redirect("/onboarding");
}

原因分析

服务器端渲染Root Layout时,已经拿到了最新的session并传递给SessionProvider,但客户端的useSession初始值是SSR时的快照。注册完成重定向后,客户端没有自动触发session刷新,导致status仍为unauthenticated,直到手动刷新或主动调用update()。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 21:55:10