Skip to content

Commit edb9a13

Browse files
committed
feat: Introducing adapters for other frameworks
BREAKING CHANGE: nuqs now requires wrapping your app with a NuqsAdapter, which is a context provider connecting your framework APIs to the hooks' internals. BREAKING CHANGE: The `startTransition` option no longer automatically sets `shallow: false`. The `Options` type is no longer generic. BREAKING CHANGE: The "use client" directive was not included in the client import (`import {} from 'nuqs'`). It has now been added, meaning that server-side code needs to import from `nuqs/server` to avoid errors like: ``` Error: Attempted to call withDefault() from the server but withDefault is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component. ``` Closes #603, #620.
1 parent 97a1ea8 commit edb9a13

32 files changed

+493
-227
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ package-lock.json
99
.next/
1010
.turbo/
1111
.vercel
12+
.tsbuildinfo

errors/NUQS-404.md

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# `nuqs` requires an adapter to work with your framework
2+
3+
## Probable cause
4+
5+
You haven't wrapped the components calling `useQueryState(s)` with
6+
an adapter.
7+
8+
Adapters are based on React Context, and provide nuqs hooks with
9+
the interfaces to work with your framework.
10+
11+
## Possible solutions
12+
13+
Follow the setup instructions to import and wrap your application
14+
using a suitable adapter.
15+
16+
Example, for Next.js (app router)
17+
18+
```tsx
19+
// src/app/layout.tsx
20+
import React from 'react'
21+
import { NuqsAdapter } from 'nuqs/adapters/next'
22+
23+
export default function RootLayout({
24+
children
25+
}: {
26+
children: React.ReactNode
27+
}) {
28+
return (
29+
<html>
30+
<body>
31+
<NuqsAdapter>{children}</NuqsAdapter>
32+
</body>
33+
</html>
34+
)
35+
}
36+
```
37+
38+
### Test adapter
39+
40+
If you encounter this error outside of the browser, like in a test
41+
runner, you may use the test adapter from `nuqs/adapters/test`
42+
to mock the context and access setup/assertion testing facilities.
43+
44+
```tsx
45+
46+
```

packages/docs/content/docs/options.mdx

+9-2
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ to get loading states while the server is re-rendering server components with
130130
the updated URL.
131131

132132
Pass in the `startTransition` function from `useTransition` to the options
133-
to enable this behaviour _(this will set `shallow: false{:ts}` automatically for you)_:
133+
to enable this behaviour:
134134

135135
```tsx /startTransition/1,3#2
136136
'use client'
@@ -144,7 +144,7 @@ function ClientComponent({ data }) {
144144
const [query, setQuery] = useQueryState(
145145
'query',
146146
// 2. Pass the `startTransition` as an option:
147-
parseAsString().withOptions({ startTransition })
147+
parseAsString().withOptions({ startTransition, shallow: false })
148148
)
149149
// 3. `isLoading` will be true while the server is re-rendering
150150
// and streaming RSC payloads, when the query is updated via `setQuery`.
@@ -157,6 +157,13 @@ function ClientComponent({ data }) {
157157
}
158158
```
159159

160+
<Callout>
161+
In `nuqs@1.x.x`, passing `startTransition` as an option automatically sets
162+
`shallow: false{:ts}`.
163+
164+
This is no longer the case in `nuqs@>=2.0.0`: you'll need to set it explicitly.
165+
</Callout>
166+
160167
## Clear on default
161168

162169
By default, when the state is set to the default value, the search parameter is

packages/docs/src/app/playground/_demos/throttling/client.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function Client() {
1313
const [serverDelay, setServerDelay] = useQueryState(
1414
'serverDelay',
1515
delayParser.withOptions({
16+
shallow: false,
1617
startTransition: startDelayTransition
1718
})
1819
)
@@ -23,6 +24,7 @@ export function Client() {
2324
const [q, setQ] = useQueryState(
2425
'q',
2526
queryParser.withOptions({
27+
shallow: false,
2628
throttleMs: clientDelay,
2729
startTransition: startQueryTransition
2830
})

packages/docs/src/components/ui/toggle-group.tsx

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
"use client"
1+
'use client'
22

3-
import * as React from "react"
4-
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5-
import { VariantProps } from "class-variance-authority"
3+
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
4+
import { VariantProps } from 'class-variance-authority'
5+
import * as React from 'react'
66

7-
import { cn } from "@/src/lib/utils"
8-
import { toggleVariants } from "@/src/components/ui/toggle"
7+
import { toggleVariants } from '@/src/components/ui/toggle'
8+
import { cn } from '@/src/lib/utils'
99

1010
const ToggleGroupContext = React.createContext<
1111
VariantProps<typeof toggleVariants>
1212
>({
13-
size: "default",
14-
variant: "default",
13+
size: 'default',
14+
variant: 'default'
1515
})
1616

1717
const ToggleGroup = React.forwardRef<
@@ -21,11 +21,11 @@ const ToggleGroup = React.forwardRef<
2121
>(({ className, variant, size, children, ...props }, ref) => (
2222
<ToggleGroupPrimitive.Root
2323
ref={ref}
24-
className={cn("flex items-center justify-center gap-1", className)}
24+
className={cn('flex items-center justify-center gap-1', className)}
2525
{...props}
2626
>
2727
<ToggleGroupContext.Provider value={{ variant, size }}>
28-
{children}
28+
<>{children}</>
2929
</ToggleGroupContext.Provider>
3030
</ToggleGroupPrimitive.Root>
3131
))
@@ -45,7 +45,7 @@ const ToggleGroupItem = React.forwardRef<
4545
className={cn(
4646
toggleVariants({
4747
variant: context.variant || variant,
48-
size: context.size || size,
48+
size: context.size || size
4949
}),
5050
className
5151
)}

packages/e2e/src/app/app/push/searchParams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseAsInteger } from 'nuqs'
1+
import { parseAsInteger } from 'nuqs/server'
22

33
export const parser = parseAsInteger.withDefault(0).withOptions({
44
history: 'push'

packages/e2e/src/app/app/transitions/client.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
'use client'
22

33
import { parseAsInteger, useQueryState } from 'nuqs'
4-
import React from 'react'
4+
import { useTransition } from 'react'
55
import { HydrationMarker } from '../../../components/hydration-marker'
66

77
export function Client() {
8-
const [isLoading, startTransition] = React.useTransition()
8+
const [isLoading, startTransition] = useTransition()
99
const [counter, setCounter] = useQueryState(
1010
'counter',
11-
parseAsInteger.withDefault(0).withOptions({ startTransition })
11+
parseAsInteger.withDefault(0).withOptions({
12+
shallow: false,
13+
startTransition
14+
})
1215
)
1316
return (
1417
<>

packages/e2e/src/app/layout.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { NuqsAdapter } from 'nuqs/adapters/next'
12
import React, { Suspense } from 'react'
23
import { HydrationMarker } from '../components/hydration-marker'
34

@@ -16,7 +17,7 @@ export default function RootLayout({
1617
<Suspense>
1718
<HydrationMarker />
1819
</Suspense>
19-
{children}
20+
<NuqsAdapter>{children}</NuqsAdapter>
2021
</body>
2122
</html>
2223
)

packages/e2e/src/pages/_app.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { AppProps } from 'next/app'
2+
import { NuqsAdapter } from 'nuqs/adapters/next'
3+
4+
export default function MyApp({ Component, pageProps }: AppProps) {
5+
return (
6+
<NuqsAdapter>
7+
<Component {...pageProps} />
8+
</NuqsAdapter>
9+
)
10+
}

packages/nuqs/adapters/next.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is needed for projects that have `moduleResolution` set to `node`
2+
// in their tsconfig.json to be able to `import {} from 'nuqs/adpaters/next'`.
3+
// Other module resolutions strategies will look for the `exports` in `package.json`,
4+
// but with `node`, TypeScript will look for a .d.ts file with that name at the
5+
// root of the package.
6+
7+
export * from '../dist/adapters/next'

packages/nuqs/adapters/react.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is needed for projects that have `moduleResolution` set to `node`
2+
// in their tsconfig.json to be able to `import {} from 'nuqs/adapters/react'`.
3+
// Other module resolutions strategies will look for the `exports` in `package.json`,
4+
// but with `node`, TypeScript will look for a .d.ts file with that name at the
5+
// root of the package.
6+
7+
export * from '../dist/adapters/react'

packages/nuqs/adapters/testing.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This file is needed for projects that have `moduleResolution` set to `node`
2+
// in their tsconfig.json to be able to `import {} from 'nuqs/adpaters/testing'`.
3+
// Other module resolutions strategies will look for the `exports` in `package.json`,
4+
// but with `node`, TypeScript will look for a .d.ts file with that name at the
5+
// root of the package.
6+
7+
export * from '../dist/adapters/testing'

packages/nuqs/package.json

+27-7
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
},
2929
"files": [
3030
"dist/",
31-
"parsers.d.ts",
32-
"server.d.ts"
31+
"server.d.ts",
32+
"adapters/react.d.ts",
33+
"adapters/next.d.ts",
34+
"adapters/testing.d.ts"
3335
],
3436
"type": "module",
3537
"sideEffects": false,
@@ -43,11 +45,24 @@
4345
"./server": {
4446
"types": "./dist/server.d.ts",
4547
"import": "./dist/server.js"
48+
},
49+
"./adapters/react": {
50+
"types": "./dist/adapters/react.d.ts",
51+
"import": "./dist/adapters/react.js"
52+
},
53+
"./adapters/next": {
54+
"types": "./dist/adapters/next.d.ts",
55+
"import": "./dist/adapters/next.js"
56+
},
57+
"./adapters/testing": {
58+
"types": "./dist/adapters/testing.d.ts",
59+
"import": "./dist/adapters/testing.js"
4660
}
4761
},
4862
"scripts": {
49-
"dev": "tsup --watch --external=react",
50-
"build": "tsup --clean --external=react",
63+
"dev": "tsup --watch",
64+
"prebuild": "rm -rf dist",
65+
"build": "tsup",
5166
"postbuild": "size-limit --json > size.json",
5267
"test": "run-p test:*",
5368
"test:types": "tsd",
@@ -56,13 +71,16 @@
5671
"prepack": "./scripts/prepack.sh"
5772
},
5873
"peerDependencies": {
59-
"next": ">= 14.1.2",
6074
"react": ">= 18.2.0"
6175
},
6276
"dependencies": {
6377
"mitt": "^3.0.1"
6478
},
79+
"optionalDependencies": {
80+
"next": ">= 14.1.2"
81+
},
6582
"devDependencies": {
83+
"@microsoft/api-extractor": "^7.47.9",
6684
"@size-limit/preset-small-lib": "^11.1.4",
6785
"@types/node": "^20.16.3",
6886
"@types/react": "^18.3.5",
@@ -87,15 +105,17 @@
87105
"path": "dist/index.js",
88106
"limit": "5 kB",
89107
"ignore": [
90-
"react"
108+
"react",
109+
"next"
91110
]
92111
},
93112
{
94113
"name": "Server",
95114
"path": "dist/server.js",
96115
"limit": "2 kB",
97116
"ignore": [
98-
"react"
117+
"react",
118+
"next"
99119
]
100120
}
101121
]

packages/nuqs/src/adapters/defs.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Options } from '../defs'
2+
3+
export type AdapterOptions = Pick<Options, 'history' | 'scroll' | 'shallow'>
4+
5+
export type UpdateUrlFunction = (
6+
search: URLSearchParams,
7+
options: Required<AdapterOptions>
8+
) => void
9+
10+
export type UseAdapterHook = () => AdapterInterface
11+
12+
export type AdapterInterface = {
13+
searchParams: URLSearchParams
14+
updateUrl: UpdateUrlFunction
15+
rateLimitFactor?: number
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createContext, createElement, useContext, type ReactNode } from 'react'
2+
import { error } from '../errors'
3+
import type { UseAdapterHook } from './defs'
4+
5+
export type AdapterContext = {
6+
useAdapter: UseAdapterHook
7+
}
8+
9+
export const context = createContext<AdapterContext>({
10+
useAdapter() {
11+
throw new Error(error(404))
12+
}
13+
})
14+
context.displayName = 'NuqsAdapterContext'
15+
16+
export function createAdapterProvider(useAdapter: UseAdapterHook) {
17+
return ({ children, ...props }: { children: ReactNode }) =>
18+
createElement(
19+
context.Provider,
20+
{ ...props, value: { useAdapter } },
21+
children
22+
)
23+
}
24+
25+
export function useAdapter() {
26+
const value = useContext(context)
27+
if (!('useAdapter' in value)) {
28+
throw new Error(error(404))
29+
}
30+
return value.useAdapter()
31+
}

0 commit comments

Comments
 (0)