NextJS 13 App目录下如何实现加载指示器或进度条?
在Next.js 13 App Directory架构下实现客户端路由加载进度条/指示器
问题根源
App Directory中,根目录的loading.tsx仅针对Server Components的SSR/SSG加载生效,而客户端组件页面(如首页、登录/注册页)的路由跳转是在客户端侧完成的,不会触发loading.tsx。旧的next-n-progress失效是因为它依赖Pages Router的路由事件机制,而App Directory使用了新的next/navigation路由API。
解决方案一:手动实现自定义进度条(无依赖)
创建一个客户端组件,通过监听App Directory的路由事件来控制进度条显示:
- 创建
components/ProgressBar.tsx(必须标记为客户端组件):
'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; export default function ProgressBar() { const [isLoading, setIsLoading] = useState(false); const router = useRouter(); 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]); return ( <div className={`fixed top-0 left-0 right-0 h-1 bg-indigo-600 transition-all duration-500 ease-out ${ isLoading ? 'w-full' : 'w-0' }`} /> ); }
- 在根
layout.tsx中全局引入:
import ProgressBar from '@/components/ProgressBar'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh-CN"> <body> <ProgressBar /> {children} </body> </html> ); }
解决方案二:适配next-n-progress到App Directory
如果偏好使用next-n-progress,可以通过客户端组件重新封装,适配新的路由事件:
- 安装依赖:
npm install nprogress
- 创建
components/NProgressWrapper.tsx:
'use client'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; export default function NProgressWrapper() { const router = useRouter(); useEffect(() => { const handleStart = () => NProgress.start(); const handleEnd = () => NProgress.done(); router.events.on('routeChangeStart', handleStart); router.events.on('routeChangeComplete', handleEnd); router.events.on('routeChangeError', handleEnd); return () => { router.events.off('routeChangeStart', handleStart); router.events.off('routeChangeComplete', handleEnd); router.events.off('routeChangeError', handleEnd); }; }, [router]); return null; }
- 同样在根
layout.tsx中引入该组件即可。
解决方案三:适配MUI的CSS-in-JS方案
如果项目使用MUI,可以直接用MUI的LinearProgress组件实现进度条,保持风格统一:
'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import LinearProgress from '@mui/material/LinearProgress'; import { Box } from '@mui/material'; export default function MUIProgressBar() { const [isLoading, setIsLoading] = useState(false); const router = useRouter(); useEffect(() => { const handleStart = () => setIsLoading(true); const handleEnd = () => setIsLoading(false); router.events.on('routeChangeStart', handleStart); router.events.on('routeChangeComplete', handleEnd); router.events.on('routeChangeError', handleEnd); return () => { router.events.off('routeChangeStart', handleStart); router.events.off('routeChangeComplete', handleEnd); router.events.off('routeChangeError', handleEnd); }; }, [router]); return isLoading ? ( <Box sx={{ width: '100%', position: 'fixed', top: 0, left: 0, zIndex: 9999, '& .MuiLinearProgress-root': { height: 3 } }}> <LinearProgress color="primary" /> </Box> ) : null; }
关键注意事项
- 所有监听路由事件的组件必须标记为
'use client',因为useRouter只能在客户端组件中使用。 - 根
layout.tsx作为Server Component,直接导入客户端组件是允许的,Next.js会自动处理组件边界。 - 该方案与
loading.tsx互补:SSR页面用loading.tsx,客户端路由跳转用进度条,覆盖所有加载场景。
内容的提问来源于stack exchange,提问作者Yassine Chettouch
相关产品推荐
相关产品推荐

