nuqs
nuqs copied to clipboard
useSearchParams() should be wrapped in a suspense boundary
Context
What's your version of nuqs?
"nuqs": "^1.16.1",
Next.js information (obtained by running next info):
Operating System:
Platform: win32
Arch: x64
Version: Windows 10 Pro
Binaries:
Node: 20.9.0
npm: N/A
Yarn: N/A
pnpm: N/A
Relevant Packages:
next: 14.1.0
eslint-config-next: 14.0.4
react: 18.2.0
react-dom: 18.2.0
typescript: 5.2.2
Next.js Config:
output: standalone
Are you using:
- ✅ The app router
- ❌ The pages router
- ❌ The
basePathoption in your Next.js config - ❌ The experimental
windowHistorySupportflag in your Next.js config
Description
Hi, after updating nextjs to 14.1.0 and nuqs to 1.16.1 I get useSearchParams() should be wrapped in a suspense boundary error at multiple pages and I'm not using useSearchParams() directly in my code anywhere. So its comming from nuqs. also I read the nextjs documentation that says: Ensure that calls to useSearchParams() are wrapped in a Suspense boundary. But this can not be done with my code. should I disable it in next config or there is another way to fix this error.
I encountered this issue as well when updating to Next.js 14.1.0 a couple of hours after its release, and forgot about it. Thanks for reminding me, this needs addressing in the docs.
The root of the problem is that Next.js 14.1.0 now requires all client components to be wrapped in a <Suspense> boundary. Using client-side hooks automatically opts the calling component as being a client component.
nuqs hooks use useSearchParams internally, and are bound to the same paradigm.
Folks are most likely to encounter this issue when using a top-level 'use client' directive in the page.tsx file and using client-side hooks in the default export (the Page component). It's a common pattern that now requires an intermediate step:
// page.tsx
'use client'
export default function Page() {
const [foo, setFoo] = useQueryState('foo')
// ...
}
Should now become:
// page.tsx
'use client'
export default function Page() {
return (
<Suspense>
<Client />
</Suspense>
)
}
function Client() {
const [foo, setFoo] = useQueryState('foo')
// ...
}
This is not the only solution: the recommended way would be to have page.tsx be a server component (no 'use client' directive), and have it import a separate file (eg client.tsx) that uses client-side features, but the approach outlined above requires fewer changes. That being said, in the future Next.js may also decide that 'use client' is banned from top-level page.tsx files.
Another refactoring approach is to move the useQueryState(s) hooks as deep in the tree as possible. Since they are synced together, you can even split reads and writes, and locate them as close to their usage as needed:
// /app/foo/_components/searchBar.tsx
'use client'
export function SearchBar() {
const [search, setSearch] = useQueryState('q', { defaultValue: '' })
return <input value={search} onChange={e => setSearch(e.target.value)} />
}
// /app/foo/_components/searchResults.tsx
'use client'
export function SearchResults() {
const [search] = useQueryState('q', { defaultValue: '' }) // read-only
// ...
}
I wrap all client components in Suspense and then add them to page.jsx but I still get the error. So I had to disable it in next.config.js for now.
In my case I wasn't using 'use client'; on any of my pages. I noticed pages that had 0 client side components such as my loading.tsx page were triggering this nextjs error.
The fix: I worked my way through the layout.tsx file, wrapping the top context provider in <suspense> and verified that the build passed, then slowly worked down the tree until I found the actual problem. The actual problem ended up being a search component (which used nuqs) in my <Header/> component.
tldr: I recommend starting in your layout.tsx and working down the tree.
There are still a few days of Hacktoberfest left, so this issue could be a good occasion for someone to update the docs to point to the Next.js docs indicating why we need to wrap client components in <Suspense> (or rather why you get this error when using nuqs).
Hey would like to work on this can you brief me more about it?
@HunainSiddiqui Sure, thanks!
The issue is that this error pops up if people use the nuqs hooks without having wrapped their client components in a Suspense boundary: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
Make yourself familiar with this issue thread, maybe play around with Next.js and nuqs hooks to trigger the error, and feel free to open a PR in the docs (the troubleshooting page would be a good place). No fully-generated ChatGPT/LLM content, please.
Have a look at this if any changes are required. If all is correct I will make a PR.
:tada: This issue has been resolved in version 2.1.2 :tada:
The release is available on:
Your semantic-release bot :package::rocket:
Getting this error on "nuqs": "^2.2.3".
Using suspense did solve my problem. But still, why wouldn't useQueryState just use the default value?
Doesn't make sense. Needing to wrap the whole thing in suspense adds a lot of layout shifts
@Nusab19 You would have this issue with useState too, it's not specific to nuqs, and there is nothing to fix on our side, it's a design choice by the React/Next.js teams.
useQueryState is a client-side hook, which means it's bundled into a .js script that needs to be sent to the client to be run there. This takes time (more so on slower/spotty network connections), and it's the reason why you would want to show a fallback UI using Suspense, to avoid blocking the rest of the page.
Needing to wrap the whole thing in suspense adds a lot of layout shifts
One way to avoid this is placing Suspense boundaries as low as possible in the React tree (minimising both the client bundle size and the layout shift sites), and designing fallback UIs that follow the same layout as your main UI. That last part might not always be possible on dynamic/user-generated data, I agree.
But still, why wouldn't useQueryState just use the default value?
It does read the value (whether from the URL or falling back to the defaultValue provided), once the client bundle has loaded. But the browser still needs that code to do so.
@franky47 Got it.
Though, just checked if useState throws the same error, it doesn't. :>
I understand your point on why it's happening. But is there a way to load only the nuqs bundle before the page becomes interactive?
And nuqs is not that big in size. Is there any way to prefetch the nuqs bundle?
I used some skeleton's to minimize the layout shifts. So it's not that big of a deal now.
Another thing I forgot to say, I love nuqs tho. It has made my work much more easier. ( Apart from that Suspense thing :3 )
Though, just checked if useState throws the same error, it doesn't. :>
My bad, it's indeed a Next.js specific thing, coming from the internal use of useSearchParams.
Doing what you mentioned might be possible, but the amount of hoops you'd have to jump through to eject from the app router "way of doing things" is probably worth less than setting up proper Suspense boundaries (especially with upcoming features like PPR and dynamicIO / "use cache").
It doesn't seem right to always require a suspense boundary. It's forcing us to show loading spinners when a default value would be perfectly fine.
Here's the dumb solution that seems to work fine for me:
import {useQueryState, UseQueryStateOptions} from 'nuqs'
export const useQueryStateWithoutSuspense = ((key: string, options: UseQueryStateOptions<{}>) => {
try {
return useQueryState(key, options)
} catch (err) {
if (String(err).includes(`Bail out to client-side rendering: useSearchParams()`)) {
return [null, () => {}] as never
}
throw err
}
}) as typeof useQueryState
export default function MyPage() {
const [query, setQuery] = useQueryStateWithoutSuspense('query')
return <input value={query} onChange={ev => setQuery(ev.target.value)} />
}
I'm not sure if nextjs should fix this or nuqs should but it's silly.