Next.js中如何仅首次加载获取用户数据?路由切换重复触发问题
Next.js 中实现仅首次加载获取用户数据的最优方案
问题根源
你遇到的重复触发请求、加载状态反复出现的问题,核心原因要么是AuthProvider没全局挂载(比如每个页面都单独套了一层),要么是初始化控制逻辑没彻底拦住重复执行。你用useRef标记初始化的思路是对的,但需要结合全局挂载和状态优化来落地。
解决步骤
1. 先把AuthProvider全局唯一挂载
把AuthProvider放在根layout.tsx里,确保整个应用只初始化一次。如果根layout是服务端组件,直接导入客户端的AuthProvider就行:
// app/layout.tsx import { AuthProvider } from '@/components/AuthProvider'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="pt-BR"> <body> <AuthProvider>{children}</AuthProvider> </body> </html> ); }
2. 优化AuthProvider的初始化与状态逻辑
修改你的AuthProvider代码,强化初始化控制,同时优化loading展示的时机:
'use client' import { User } from '@/models/user' import { AuthService } from '@/services/auth-service' import { createContext, ReactNode, useCallback, useEffect, useRef, useState, } from 'react' import { setCookie, destroyCookie, parseCookies } from 'nookies' import { useRouter } from 'next/navigation' import http from '@/utils/axios' import { UserService } from '@/services/user-service' import { jwtDecode } from 'jwt-decode' import { toast } from 'sonner' interface AuthContextType { isAuthenticated: boolean user: User | null signIn: (email: string, password: string) => Promise<void> signOut: () => void isLoadingInitial: boolean // 专门标记首次加载状态 } interface AuthProviderProps { children: ReactNode } export const AuthContext = createContext({} as AuthContextType) export const AuthProvider = ({ children }: AuthProviderProps) => { const router = useRouter() const hasFetchedUser = useRef(false) // 标记是否已经获取过用户数据 const [user, setUser] = useState<User | null>(null) const [isLoadingInitial, setIsLoadingInitial] = useState(true) // 只管控首次启动的加载 const isAuthenticated = !!user const signIn = async (email: string, password: string) => { const response = await AuthService.login(email, password) const decodedToken = jwtDecode(response.token) if (!decodedToken.exp) { toast('Erro ao fazer login') return } destroyCookie(undefined, 'token') const tokenExpiration = (decodedToken?.exp * 1000 - Date.now()) / 1000 setCookie(undefined, 'token', response.token, { maxAge: tokenExpiration, }) http.defaults.headers.Authorization = `Bearer ${response.token}` setUser(response.user) router.replace('/rece') } const signOut = useCallback(() => { destroyCookie(undefined, 'token') destroyCookie(undefined, 'refreshToken') setUser(null) router.replace('/login') }, [router]) useEffect(() => { // 已经获取过用户数据就直接返回,不再执行 if (hasFetchedUser.current) return hasFetchedUser.current = true const initAuth = async () => { try { const cookies = parseCookies() const token = cookies['token'] if (token) { http.defaults.headers.Authorization = `Bearer ${token}` const me = await UserService.getMe() if (me) setUser(me) } } catch (error) { // 处理token无效的情况,直接清掉过期token并提示 destroyCookie(undefined, 'token') toast('Sessão expirada, faça login novamente') } finally { // 不管成功失败,都结束初始加载状态 setIsLoadingInitial(false) } } initAuth() }, []) // 只有首次加载时才显示Loading,路由切换时不会再触发 if (isLoadingInitial) { return <>Loading</> } return ( <AuthContext.Provider value={{ isAuthenticated, user, signIn, signOut, isLoadingInitial }}> {children} </AuthContext.Provider> ) }
3. 关键优化点说明
- 全局挂载:确保AuthProvider只在根layout里挂载一次,路由切换时不会重新初始化。
useRef持久化标记:hasFetchedUser会一直保留状态,哪怕组件重渲染也不会重置,彻底拦住重复请求。- 拆分loading状态:专门用
isLoadingInitial管控首次启动的加载,路由切换时不再显示加载提示,提升流畅性。 - 错误处理:初始化时捕获接口错误,自动清理无效token,避免用户停留在无效状态。
额外提示
- 登录成功后直接用接口返回的用户数据就行,不用再调用
getMe,减少一次请求。 - 如果要处理token过期,可以加个refresh token机制,初始化时自动刷新有效token,不用用户重新登录。
内容的提问来源于stack exchange,提问作者Nilton Schumacher F
相关产品推荐
相关产品推荐

