diff --git a/packages/e2e/src/app/app/cache/page.tsx b/packages/e2e/src/app/app/cache/page.tsx index c714d1e2..f4d50920 100644 --- a/packages/e2e/src/app/app/cache/page.tsx +++ b/packages/e2e/src/app/app/cache/page.tsx @@ -1,3 +1,4 @@ +import type { SearchParams } from 'nuqs/server' import { Suspense } from 'react' import { All } from './all' import { Get } from './get' @@ -5,11 +6,11 @@ import { cache } from './searchParams' import { Set } from './set' type Props = { - searchParams: Record + searchParams: Promise } -export default function Page({ searchParams }: Props) { - const { str, bool, num, def, nope } = cache.parse(searchParams) +export default async function Page({ searchParams }: Props) { + const { str, bool, num, def, nope } = await cache.parse(searchParams) return ( <>

Root page

@@ -30,9 +31,9 @@ export default function Page({ searchParams }: Props) { ) } -export function generateMetadata({ searchParams }: Props) { +export async function generateMetadata({ searchParams }: Props) { // parse here too to ensure we can idempotently parse the same search params as the page in the same request - const { str } = cache.parse(searchParams) + const { str } = await cache.parse(searchParams) return { title: `metadata-title-str:${str}` } diff --git a/packages/e2e/src/app/app/push/page.tsx b/packages/e2e/src/app/app/push/page.tsx index 2a0f0120..54e1e567 100644 --- a/packages/e2e/src/app/app/push/page.tsx +++ b/packages/e2e/src/app/app/push/page.tsx @@ -1,12 +1,13 @@ +import type { SearchParams } from 'nuqs/server' import { Client } from './client' -import { parser } from './searchParams' +import { searchParamsCache } from './searchParams' -export default function Page({ +export default async function Page({ searchParams }: { - searchParams: Record + searchParams: Promise }) { - const server = parser.parseServerSide(searchParams.server) + const { server } = await searchParamsCache.parse(searchParams) return ( <>

diff --git a/packages/e2e/src/app/app/push/searchParams.ts b/packages/e2e/src/app/app/push/searchParams.ts index 8aaf9946..26e0548b 100644 --- a/packages/e2e/src/app/app/push/searchParams.ts +++ b/packages/e2e/src/app/app/push/searchParams.ts @@ -1,5 +1,8 @@ -import { parseAsInteger } from 'nuqs' +import { createSearchParamsCache, parseAsInteger } from 'nuqs/server' export const parser = parseAsInteger.withDefault(0).withOptions({ history: 'push' }) +export const searchParamsCache = createSearchParamsCache({ + server: parser +}) diff --git a/packages/e2e/src/app/app/rewrites/destination/page.tsx b/packages/e2e/src/app/app/rewrites/destination/page.tsx index 5237eaf6..f1d625d1 100644 --- a/packages/e2e/src/app/app/rewrites/destination/page.tsx +++ b/packages/e2e/src/app/app/rewrites/destination/page.tsx @@ -3,12 +3,12 @@ import { Suspense } from 'react' import { RewriteDestinationClient } from './client' import { cache } from './searchParams' -export default function RewriteDestinationPage({ +export default async function RewriteDestinationPage({ searchParams }: { - searchParams: SearchParams + searchParams: Promise }) { - const { injected, through } = cache.parse(searchParams) + const { injected, through } = await cache.parse(searchParams) return ( <>

diff --git a/packages/e2e/src/app/app/transitions/page.tsx b/packages/e2e/src/app/app/transitions/page.tsx index 3a4112f6..bd291f26 100644 --- a/packages/e2e/src/app/app/transitions/page.tsx +++ b/packages/e2e/src/app/app/transitions/page.tsx @@ -1,9 +1,10 @@ import { setTimeout } from 'node:timers/promises' +import type { SearchParams } from 'nuqs/server' import { Suspense } from 'react' import { Client } from './client' type PageProps = { - searchParams: Record + searchParams: Promise } export default async function Page({ searchParams }: PageProps) { @@ -11,7 +12,7 @@ export default async function Page({ searchParams }: PageProps) { return ( <>

Transitions

-
{JSON.stringify(searchParams)}
+
{JSON.stringify(await searchParams)}
diff --git a/packages/nuqs/src/cache.ts b/packages/nuqs/src/cache.ts index d5e1beb3..a54eab64 100644 --- a/packages/nuqs/src/cache.ts +++ b/packages/nuqs/src/cache.ts @@ -1,22 +1,17 @@ import { cache } from 'react' import { error } from './errors' -import type { ParserBuilder } from './parsers' +import type { ParserBuilder, inferParserType } from './parsers' export type SearchParams = Record const $input: unique symbol = Symbol('Input') -type ExtractParserType = - Parser extends ParserBuilder - ? ReturnType - : never - export function createSearchParamsCache< Parsers extends Record> >(parsers: Parsers) { type Keys = keyof Parsers type ParsedSearchParams = { - [K in Keys]: ExtractParserType + readonly [K in Keys]: inferParserType } type Cache = { @@ -32,7 +27,8 @@ export function createSearchParamsCache< const getCache = cache<() => Cache>(() => ({ searchParams: {} })) - function parse(searchParams: SearchParams) { + + function parseSync(searchParams: SearchParams): ParsedSearchParams { const c = getCache() if (Object.isFrozen(c.searchParams)) { // Parse has already been called... @@ -51,14 +47,43 @@ export function createSearchParamsCache< c.searchParams[key] = parser.parseServerSide(searchParams[key]) } c[$input] = searchParams - return Object.freeze(c.searchParams) as Readonly + return Object.freeze(c.searchParams) as ParsedSearchParams + } + + /** + * Parse the incoming `searchParams` page prop using the parsers provided, + * and make it available to the RSC tree. + * + * @returns The parsed search params for direct use in the page component. + * + * Note: Next.js 15 introduced a breaking change in making their + * `searchParam` prop a Promise. You will need to await this function + * to use the Promise version in Next.js 15. + */ + function parse(searchParams: SearchParams): ParsedSearchParams + + /** + * Parse the incoming `searchParams` page prop using the parsers provided, + * and make it available to the RSC tree. + * + * @returns The parsed search params for direct use in the page component. + * + * Note: this async version requires Next.js 15 or later. + */ + function parse(searchParams: Promise): Promise + + function parse(searchParams: SearchParams | Promise) { + if (searchParams instanceof Promise) { + return searchParams.then(parseSync) + } + return parseSync(searchParams) } function all() { const { searchParams } = getCache() if (Object.keys(searchParams).length === 0) { throw new Error(error(500)) } - return searchParams as Readonly + return searchParams as ParsedSearchParams } function get(key: Key): ParsedSearchParams[Key] { const { searchParams } = getCache() diff --git a/packages/nuqs/src/tests/cache.test-d.ts b/packages/nuqs/src/tests/cache.test-d.ts index 2812f8bf..ca99adb8 100644 --- a/packages/nuqs/src/tests/cache.test-d.ts +++ b/packages/nuqs/src/tests/cache.test-d.ts @@ -29,4 +29,8 @@ import { type All = Readonly<{ foo: string | null; bar: number | null; egg: boolean }> expectType(cache.parse({})) expectType(cache.all()) + + // It supports async search params (Next.js 15+) + expectType>(cache.parse(Promise.resolve({}))) + expectType(cache.all()) }