How to use nuqs without Suspense in Next.js?

This post might have stale content, as HEAD is 26 commits ahead.

Is there a way to use nuqs without requiring Suspense in Next.js App Router? How to avoid the Suspense boundary requirement?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Perfect! Now I have a complete picture. Let me provide a comprehensive answer based on the source code analysis.

##Answer

Yes, there are two ways to use nuqs without requiring Suspense boundaries for your entire page component:

###1. Use 'use client' Directive (Immediate Fix)

The quickest way is to mark your entire page as a client component. This avoids the Suspense boundary requirement because client components don't have the same SSR constraints as server components:

'use client'

import { useQueryState } from 'nuqs'

export default function Page() {
  const [count, setCount] = useQueryState('count', parseAsInteger)
  return (
    <>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => (c ?? 0) + 1)}>+</button>
    </>
  )
}

This is the optimal approach for better UX. Instead of using client-side hooks in the page component, use the createLoader function to parse search params server-side, then pass the result to client components:

// app/search-params.ts
import { createLoader, parseAsInteger } from 'nuqs/server'

export const coordinatesSearchParams = {
  latitude: parseAsInteger.withDefault(0),
  longitude: parseAsInteger.withDefault(0)
}

export const loadSearchParams = createLoader(coordinatesSearchParams)
// app/page.tsx
import { loadSearchParams } from './search-params'
import type { SearchParams } from 'nuqs/server'

type PageProps = {
  searchParams: Promise<SearchParams>
}

export default async function Page({ searchParams }: PageProps) {
  // Option A: Await immediately (no Suspense needed)
  const { latitude, longitude } = await loadSearchParams(searchParams)
  return <Map lat={latitude} lng={longitude} />

  // Option B: Pass the promise to children wrapped in Suspense (better UX)
  // This streams the outer shell immediately while loading dynamic content
}

###Why Suspense is Required with useQueryState Hooks

Looking at the source code (packages/nuqs/src/adapters/next/impl.app.ts), the hooks use useSearchParams() from Next.js:

export function useNuqsNextAppRouterAdapter(): AdapterInterface {
  const router = useRouter()
  const searchParams = useSearchParams()  // <- This requires Suspense
  // ...
}

Since useSearchParams() is a client hook that triggers Client-Side Rendering (CSR), Next.js requires it to be wrapped in a Suspense boundary when used in server components. This is Next.js's way of handling hydration and SSR properly.

###Summary

ApproachUse CaseSuspense Required
'use client' on pageSimple client-side apps❌ No
createLoader + awaitServer-side parsing with no streaming❌ No
createLoader + pass Promise to childrenOptimal UX with progressive streaming✅ Yes, but only for children
useQueryState in server components❌ Not recommended✅ Yes, mandatory

The server-side loader approach (createLoader) is recommended because it allows you to handle search params on the server while maintaining the benefits of Server-Side Rendering (SSR) and streaming.


END OF POST

How to use nuqs without Suspense in Next.js? — 47ng/nuqs