next.js
next.js copied to clipboard
Next 13: router.push with different searchParams does not trigger suspense fallback
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.)
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>
);
}
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
I just run into the same issue, but found a workaround: adding a
key
prop toSuspense
. It seems that this makes theSuspense
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.)
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 Have you had any solutions to this issue?
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.
using next-client-router methods instead of basic useRouter hook worked for me