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

NextJS15导航栏Auth按钮加载与认证状态异常问题求助

问题描述

在NextJS 15项目中,导航栏的Sign In/Sign Out组件采用Supabase邮箱密码认证,当前表现不符合预期:

预期行为

  • 登出状态下刷新页面,仅显示Sign In按钮;
  • 登录或登出过程中,先显示lucide-react的Loader2加载图标,之后切换为对应状态的按钮。

实际异常表现

  • 登录过程中Loader2加载图标不显示;
  • 登录完成后,按钮未自动切换为Sign Out,需手动刷新页面才显示;
  • 刷新页面时,Sign In/Sign Out按钮快速切换,而非显示Loader2加载图标。

相关代码

export default function AuthButton() {
  const router = useRouter();
  const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const supabase = createClient();

  useEffect(() => {
    const { data } = supabase.auth.onAuthStateChange((event, session) => {
      if (event === "SIGNED_IN" || event === "INITIAL_SESSION") {
        setIsAuthenticated(!!session);
      } else if (event === "SIGNED_OUT") {
        setIsAuthenticated(false);
        router.push("/login");
      }
      setIsLoading(false);
    });

    return () => {
      data.subscription.unsubscribe();
    };
  }, [router, supabase.auth]);

  const handleSignOut = async () => {
    try {
      await supabase.auth.signOut();
    } catch (error) {
      console.error("Error signing out:", error);
    }
  };

  if (isAuthenticated) {
    return (
      <>
        {isLoading ? (
          <div className="pl-5 pt-3">
            <Loader2 className="mr-2 h-4 w-4 animate-spin" />
          </div>
        ) : (
          <Button variant="ghost" onClick={handleSignOut} aria-label="Sign out">
            Sign Out
          </Button>
        )}
      </>
    );
  }

  return (
    <Button asChild variant="ghost" aria-label="Sign in">
      <Link href="/login">Sign In</Link>
    </Button>
  );
}

修复方案

1. 精准控制加载状态的触发时机

原代码中onAuthStateChange回调直接将isLoading设为false,导致初始化和认证过程中无法维持加载状态。需要根据不同事件类型调整:

  • 初始化会话完成后关闭加载;
  • 登录/登出操作开始时开启加载,完成后关闭。

2. 主动获取初始会话状态

Supabase的onAuthStateChange不一定自动触发初始会话校验,组件挂载时主动调用getSession(),避免依赖事件回调的延迟。

3. 优化登出流程的加载状态

在handleSignOut方法中手动设置加载状态,确保登出过程中显示加载图标,出错时也能恢复状态。

4. 全局覆盖加载场景

将加载状态的判断提升到最外层,确保任何状态切换过程中都显示加载图标,而不是仅在已认证状态下判断。

修改后的完整代码

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { Loader2 } from 'lucide-react';
import { createClient } from '@/utils/supabase/client';

export default function AuthButton() {
  const router = useRouter();
  const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const supabase = createClient();

  useEffect(() => {
    // 主动获取初始会话状态
    const fetchInitialSession = async () => {
      const { data: { session } } = await supabase.auth.getSession();
      setIsAuthenticated(!!session);
      setIsLoading(false);
    };

    fetchInitialSession();

    // 监听认证状态变化并控制加载状态
    const { data } = supabase.auth.onAuthStateChange((event, session) => {
      switch (event) {
        case 'SIGNED_IN':
          setIsAuthenticated(true);
          setIsLoading(false);
          break;
        case 'SIGNED_OUT':
          setIsAuthenticated(false);
          setIsLoading(false);
          router.push('/login');
          break;
        case 'INITIAL_SESSION':
          setIsAuthenticated(!!session);
          setIsLoading(false);
          break;
        case 'SIGNING_IN':
        case 'SIGNING_OUT':
          setIsLoading(true);
          break;
        default:
          setIsLoading(false);
      }
    });

    return () => {
      data.subscription.unsubscribe();
    };
  }, [router, supabase.auth]);

  const handleSignOut = async () => {
    try {
      setIsLoading(true);
      await supabase.auth.signOut();
    } catch (error) {
      console.error('Error signing out:', error);
      setIsLoading(false);
    }
  };

  // 所有状态切换过程中优先显示加载图标
  if (isLoading) {
    return (
      <div className="pl-5 pt-3">
        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
      </div>
    );
  }

  if (isAuthenticated) {
    return (
      <Button variant="ghost" onClick={handleSignOut} aria-label="Sign out">
        Sign Out
      </Button>
    );
  }

  return (
    <Button asChild variant="ghost" aria-label="Sign in">
      <Link href="/login">Sign In</Link>
    </Button>
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 03:55:15