You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS 13中router.push切换路由时Suspense fallback不显示问题

问题:Next.js 13 切换赛季时 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;
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 15:54:53