Next.js 13.5.4:父级loading页面覆盖子级loading页面问题
嘿,我完全懂你现在碰到的这个麻烦——在Next.js 13.5.4的App Router里,嵌套路由的loading.tsx确实会出现父级和子级加载动画同时显示的情况,这其实是因为App Router里的loading组件是跟着路由层级走的,父路由的加载状态会默认延续到子路由的加载过程中。当你访问嵌套页面时,Next.js会先触发父路由的loading,然后才开始加载子路由的内容,这就导致两个loading叠在一起显示了。
下面给你几个实用的解决方案,你可以根据自己的项目需求选:
1. 用Suspense手动控制子路由的加载状态
这是最贴合App Router设计思路的方法,通过在父页面中用Suspense组件包裹子路由内容,指定子级专属的加载 fallback,这样父级的loading.tsx只会在父路由自身加载时生效,子路由加载时只会显示你指定的子级loading。
举个例子,假设你的父路由是app/dashboard/page.tsx,子路由是app/dashboard/child/page.tsx:
// app/dashboard/page.tsx import { Suspense } from 'react'; import ChildLoading from './child/loading'; // 引入子级的loading组件 export default function DashboardPage({ children }: { children: React.ReactNode }) { return ( <div className="dashboard-container"> {/* 父路由的固定内容 */} <h1>仪表盘首页</h1> {/* 用Suspense包裹子路由内容,指定子级加载时的fallback */} <Suspense fallback={<ChildLoading />}> {children} </Suspense> </div> ); }
之后你可以删掉子路由目录下的loading.tsx,因为已经通过Suspense指定了它的加载状态。
2. 让父级loading只在自身路由生效
如果你想保留父级和子级各自的loading.tsx,可以在父级的loading组件里判断当前路由路径,只有当访问父路由本身时才显示loading,访问子路由时就隐藏。
需要注意的是这个方法要用到客户端组件的路由钩子:
// app/dashboard/loading.tsx 'use client'; import { usePathname } from 'next/navigation'; export default function ParentLoading() { const pathname = usePathname(); // 只有当路径严格等于父路由路径时,才显示父级loading if (pathname !== '/dashboard') return null; return ( <div className="parent-loading"> <p>正在加载仪表盘...</p> </div> ); }
这样当你访问/dashboard/child时,父级的loading就不会渲染了,只会显示子级的loading.tsx。
3. 改用全局统一的loading组件
如果不想纠结嵌套路由的loading层级问题,你可以做一个全局的loading组件,监听路由变化来控制显示/隐藏,这样不管访问哪个路由,都只会显示一个全局loading,避免多个loading叠加的情况。
示例代码如下:
// app/components/GlobalLoading.tsx 'use client'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; export default function GlobalLoading() { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); useEffect(() => { // 监听路由变化事件 const handleRouteStart = () => setIsLoading(true); const handleRouteEnd = () => setIsLoading(false); router.events.on('routeChangeStart', handleRouteStart); router.events.on('routeChangeComplete', handleRouteEnd); router.events.on('routeChangeError', handleRouteEnd); // 组件卸载时移除监听 return () => { router.events.off('routeChangeStart', handleRouteStart); router.events.off('routeChangeComplete', handleRouteEnd); router.events.off('routeChangeError', handleRouteEnd); }; }, [router]); if (!isLoading) return null; return ( <div className="global-loading-overlay"> <p>页面加载中...</p> </div> ); }
然后在app/layout.tsx里引入这个组件,放在最外层即可:
// app/layout.tsx import GlobalLoading from './components/GlobalLoading'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <GlobalLoading /> {children} </body> </html> ); }
个人比较推荐第一种方案,既符合App Router的设计逻辑,又能精准控制每个路由层级的加载状态,不会出现不必要的冲突。
备注:内容来源于stack exchange,提问作者musa

