next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Next 13: router.push with different searchParams does not trigger suspense fallback

Open krsteve opened this issue 2 years ago • 1 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System: Platform: win32 Arch: x64 Version: Windows 10 Home Binaries: Node: 18.12.0 npm: N/A Yarn: N/A pnpm: N/A Relevant packages: next: 13.0.2-canary.0 eslint-config-next: 13.0.0 react: 18.2.0 react-dom: 18.2.0

What browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

next dev

Describe the Bug

I'm struggling HARD with the appDir Suspense with searchParams .

On initial render, the culprit falls back to the skeleton just fine. It's when I set new searchParams with router.push from another component, the suspense fallback never renders, and the server just sits there until data fetching gets completed.

TL; DR, Changing searchParams does't make use of server-side suspense.

I'd greatly appreciate any help!

Expected Behavior

The Foo component, (i.e., the async server component that re-fetches data on prop change triggered by new searchParams acquired via the next router,) when given new searchParams --- thusly fetches new data --- should fallback to the suspense's fallback component (e.g., a skeleton component).

Link to reproduction

https://github.com/krsteve/next-13-searchparam-bug

To Reproduce

Code that DOES REPRODUCE the problem:

page.tsx

import { Suspense } from "react";
import Foo from "./Foo";

export default function ({ searchParams }: { searchParams: { foo?: string } }) {
    return <Suspense fallback={<div>LOADING</div>}>
        {/* @ts-ignore */}
        <Foo foo={searchParams.foo} />
    </Suspense>;
}

Foo.tsx

import { Button } from "./Button";

export default async function ({ foo }: { foo?: string }) {
    // Wait 3 seconds
    await new Promise(resolve => setTimeout(resolve, 3000));
    return <div>
        <div>{foo ?? "No Params"}</div>
        <Button />
    </div>;
}

Button.tsx

'use client';

import { usePathname, useRouter } from "next/navigation";
import type { FC } from "react";

export const Button: FC = () => {
    const router = useRouter();
    const pathname = usePathname();
    const randNum = Math.ceil(Math.random() * 100);
    const setNewParams = () => {
        router.push(pathname + `?foo=${randNum}`);
    }

    return <button onClick={setNewParams}>Set New Params</button>
}
  • Using router.replace does the same thing.
  • Adding loading.tsx doesn't make any difference.
  • Manually navigating with window.location.href does work, but it's just another initial rendering. (Re-renders the whole thing.)

krsteve avatar Nov 02 '22 12:11 krsteve

I just run into the same issue, but found a workaround: adding a key prop to Suspense. It seems that this makes the Suspense component being recreated every time the search parameters change, and the fallback appears every time.

export default function ({ searchParams }: { searchParams: { foo?: string } }) {
  return (
    <Suspense key={searchParams.foo} fallback={<div>LOADING</div>}>
      {/* @ts-ignore */}
      <Foo foo={searchParams.foo} />
    </Suspense>
  );
}

ryym avatar Nov 12 '22 06:11 ryym

Same with the default layout.tsx fallback file: it's not showing when searchParams are changed with router.push

setting export const dynamic = 'force-dynamic'; for layout or page does not fix the issue

tonypizzicato avatar Nov 22 '22 21:11 tonypizzicato

I just run into the same issue, but found a workaround: adding a key prop to Suspense. It seems that this makes the Suspense component being recreated every time the search parameters change, and the fallback appears every time.

export default function ({ searchParams }: { searchParams: { foo?: string } }) {
  return (
    <Suspense key={searchParams.foo} fallback={<div>LOADING</div>}>
      {/* @ts-ignore */}
      <Foo foo={searchParams.foo} />
    </Suspense>
  );
}

This does solve the issue...! Thank you for sharing!

(Although, I still think they should address this in the document, at least.)

krsteve avatar Dec 05 '22 03:12 krsteve

I'm also experiencing this behaviour, in my case with router.replace(). Adding a key to <Suspense> fixed most of the issue but I still notice some slow behaviour with this.

In my case the <Loading> component takes a second to show after I trigger router.replace() with new searchParams. Also the url takes a while to update. Not sure what's happening.

jaapaurelio avatar Dec 27 '22 01:12 jaapaurelio

@jaapaurelio Have you had any solutions to this issue?

singleseeker avatar Jun 17 '23 00:06 singleseeker

export default function ({ searchParams }: { searchParams: { foo?: string } }) {
  return (
    <Suspense key={searchParams.foo} fallback={<div>LOADING</div>}>
      {/* @ts-ignore */}
      <Foo foo={searchParams.foo} />
    </Suspense>
  );
}

Works for me, thank you.

proninyaroslav avatar Dec 16 '23 14:12 proninyaroslav

using next-client-router methods instead of basic useRouter hook worked for me

Prains avatar Apr 16 '24 13:04 Prains