NextJS 13中router.push切换路由时Suspense fallback不显示问题
我在 Next.js 13 里有个 layout.tsx,它会根据 leagueId 获取联赛数据,这个布局被 fixtures 和 standings 两个路由共享。布局里还有个下拉菜单,列出联赛的不同赛季,选择赛季时要重新获取对应赛季的赛事/积分榜数据。
layout.tsx 代码
type Props = { children: ReactNode; params: { leagueId: string }; }; const LeagueLayout = async ({ children, params }: Props) => { const leagueId = parseInt(params.leagueId); const data = await getLeagueById<League<Seasons[]>>(leagueId, "baseball"); if (data.error) { throw new Error(data.error); } if (data.success) { const season = data.success.seasons?.[data.success.seasons?.length - 1]?.season; const seasonList = getSeasonsList(data.success.seasons); return ( <div className="flex flex-col h-screen font-sans"> <div className="sticky top-0 z-20 flex flex-col gap-3 p-3 pb-0 shadow-sm lg:gap-6 lg:p-6 lg:pb-0 bg-background"> <div className="flex items-center gap-2 py-2 lg:gap-4"> <ImageWithFallback className="w-[40px] lg:w-[50px]" src={data.success.logo} alt={`${data.success.name}-logo`} /> <h2 className="flex items-center text-lg font-medium lg:text-2xl"> {data.success.name} <span className="text-sm hidden lg:inline-block lg:text-[1rem] ml-3 text-secondary-foreground"> ({season}) </span> </h2> <div className="hidden ml-auto lg:block"> <DropDown title="seasons" data={seasonList} value={season} /> </div> </div> </div> <div className="flex-1 overflow-y-auto">{children}</div> </div> ); } }; export default LeagueLayout;
DropDown 组件代码
type Props = { title: string; data?: string[]; value: string; variant?: ButtonVariants; }; export function DropDown({ title, data, value, variant }: Props) { const [item, setItem] = useState(value); const router = useRouter(); const handleValueChange = (e: string) => { setItem(e); router.push(`/baseball/league/1/fixtures?season=${e}`, { scroll: false, }); }; return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button className="capitalize w-[130px] shadow-sm overflow-hidden" variant={variant ?? "outline"} > {title} </Button> </DropdownMenuTrigger> <DropdownMenuContent className="max-h-[200px] overflow-y-auto w-[130px]"> <DropdownMenuRadioGroup value={item} onValueChange={(e) => handleValueChange(e)} > {data?.map((v, index) => ( <DropdownMenuRadioItem className="font-sans font-medium text-center capitalize" key={index} value={v} > {v} </DropdownMenuRadioItem> ))} </DropdownMenuRadioGroup> </DropdownMenuContent> </DropdownMenu> ); }
Fixtures Page 组件代码
type Props = { searchParams: { season?: string; }; params: { leagueId: string }; }; const Page = async ({ searchParams, params }: Props) => { const season = searchParams.season || "2024"; const leagueId = parseInt(params.leagueId); const key = JSON.stringify({ ...searchParams }); const data = await getFixturesByLeagueId(leagueId, season, "baseball"); if (data.error) { throw new Error(data.error); } if (data.success) { return ( <Suspense key={key} fallback={<Loading />}> <div className="w-full h-full bg-red-600">FIXTURES</div> </Suspense> ); } }; export default Page;
核心疑问
我通过更新URL查询参数来获取对应赛季的数据,首次渲染页面时会显示fallback,但切换赛季更新URL后,明明在获取新数据,fallback却不显示。之前试过给Suspense加key,但没效果,想问下我的实现哪里有问题?
你的问题核心在于Suspense的位置错误,以及异步数据获取的时机不符合Next.js 13 App Router的规则。
1. 调整Suspense的位置
你现在把Suspense放在了异步Page组件内部,但Next.js 13中异步组件本身会被框架的Suspense机制处理,内部的Suspense不会生效。正确的做法是把Suspense包裹在同步的Page组件外层,或者将数据获取拆成独立的异步子组件,再用Suspense包裹这个子组件。
修改后的Page组件:
type Props = { searchParams: { season?: string; }; params: { leagueId: string }; }; // 拆分独立的异步数据渲染组件 async function FixturesContent({ leagueId, season }: { leagueId: number; season: string }) { const data = await getFixturesByLeagueId(leagueId, season, "baseball"); if (data.error) throw new Error(data.error); return <div className="w-full h-full bg-red-600">FIXTURES</div>; } // 同步的Page组件,负责包裹Suspense const Page = ({ searchParams, params }: Props) => { const season = searchParams.season || "2024"; const leagueId = parseInt(params.leagueId); const key = JSON.stringify({ season }); return ( <Suspense key={key} fallback={<Loading />}> <FixturesContent leagueId={leagueId} season={season} /> </Suspense> ); }; export default Page;
2. 修复DropDown中的硬编码路由
当前DropDown里的路由是硬编码的/baseball/league/1/fixtures,切换到standings页面时,下拉菜单仍会跳转到fixtures,应该动态生成路由:
// DropDown组件中修改handleValueChange逻辑 const handleValueChange = (e: string) => { setItem(e); // 基于当前路径修改查询参数,避免硬编码 router.push( { pathname: router.pathname, query: { season: e }, }, undefined, { scroll: false } ); };
3. 同步Layout中的赛季显示
当前Layout里的赛季显示固定取最后一个赛季,切换赛季后不会同步更新,需要结合URL的searchParams:
// 修改LeagueLayout的Props,接收searchParams type Props = { children: ReactNode; params: { leagueId: string }; searchParams: { season?: string }; }; const LeagueLayout = async ({ children, params, searchParams }: Props) => { const leagueId = parseInt(params.leagueId); const data = await getLeagueById<League<Seasons[]>>(leagueId, "baseball"); if (data.error) throw new Error(data.error); if (data.success) { // 优先使用URL中的赛季参数,无参数时取最后一个赛季 const defaultSeason = data.success.seasons?.[data.success.seasons.length - 1]?.season; const season = searchParams.season || defaultSeason; const seasonList = getSeasonsList(data.success.seasons); return ( <div className="flex flex-col h-screen font-sans"> {/* 其余代码不变,仅h2中的season使用同步后的变量 */} <h2 className="flex items-center text-lg font-medium lg:text-2xl"> {data.success.name} <span className="text-sm hidden lg:inline-block lg:text-[1rem] ml-3 text-secondary-foreground"> ({season}) </span> </h2> {/* ... 其余代码 */} </div> ); } };
为什么之前加key没用?
因为你把Suspense放在了异步Page组件内部,当Page重新渲染时,框架会先等待它的异步操作完成,才会渲染内部的内容,导致fallback完全没机会显示。把Suspense提到同步的Page组件外层并加key,React会在searchParams变化时重新创建Suspense节点,触发fallback显示。
内容的提问来源于stack exchange,提问作者Rayyan Alam

