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
相关产品推荐
相关产品推荐

