Skip to content

Commit

Permalink
feat: Introducing adapters for other frameworks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
franky47 committed Oct 9, 2024
1 parent 7dd62f7 commit f83f219
Show file tree
Hide file tree
Showing 32 changed files with 733 additions and 234 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ package-lock.json
.next/
.turbo/
.vercel
.tsbuildinfo
46 changes: 46 additions & 0 deletions errors/NUQS-404.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# `nuqs` requires an adapter to work with your framework

## Probable cause

You haven't wrapped the components calling `useQueryState(s)` with
an adapter.

Adapters are based on React Context, and provide nuqs hooks with
the interfaces to work with your framework.

## Possible solutions

Follow the setup instructions to import and wrap your application
using a suitable adapter.

Example, for Next.js (app router)

```tsx
// src/app/layout.tsx
import React from 'react'
import { NuqsAdapter } from 'nuqs/adapters/next'

export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
)
}
```

### Test adapter

If you encounter this error outside of the browser, like in a test
runner, you may use the test adapter from `nuqs/adapters/test`
to mock the context and access setup/assertion testing facilities.

```tsx

```
11 changes: 9 additions & 2 deletions packages/docs/content/docs/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ to get loading states while the server is re-rendering server components with
the updated URL.

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

<Callout title="Upcoming changes" type="warn">
In `[email protected]`, passing `startTransition` will no longer automatically set `shallow: false{:ts}`.
Expand All @@ -148,7 +148,7 @@ function ClientComponent({ data }) {
const [query, setQuery] = useQueryState(
'query',
// 2. Pass the `startTransition` as an option:
parseAsString().withOptions({ startTransition })
parseAsString().withOptions({ startTransition, shallow: false })
)
// 3. `isLoading` will be true while the server is re-rendering
// and streaming RSC payloads, when the query is updated via `setQuery`.
Expand All @@ -161,6 +161,13 @@ function ClientComponent({ data }) {
}
```

<Callout>
In `[email protected]`, passing `startTransition` as an option automatically sets
`shallow: false{:ts}`.

This is no longer the case in `nuqs@>=2.0.0`: you'll need to set it explicitly.
</Callout>

## Clear on default

By default, when the state is set to the default value, the search parameter is
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/src/app/playground/_demos/throttling/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function Client() {
const [serverDelay, setServerDelay] = useQueryState(
'serverDelay',
delayParser.withOptions({
shallow: false,
startTransition: startDelayTransition
})
)
Expand All @@ -23,6 +24,7 @@ export function Client() {
const [q, setQ] = useQueryState(
'q',
queryParser.withOptions({
shallow: false,
throttleMs: clientDelay,
startTransition: startQueryTransition
})
Expand Down
22 changes: 11 additions & 11 deletions packages/docs/src/components/ui/toggle-group.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"use client"
'use client'

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

import { cn } from "@/src/lib/utils"
import { toggleVariants } from "@/src/components/ui/toggle"
import { toggleVariants } from '@/src/components/ui/toggle'
import { cn } from '@/src/lib/utils'

const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
size: 'default',
variant: 'default'
})

const ToggleGroup = React.forwardRef<
Expand All @@ -21,11 +21,11 @@ const ToggleGroup = React.forwardRef<
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
className={cn('flex items-center justify-center gap-1', className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
<>{children}</>
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
Expand All @@ -45,7 +45,7 @@ const ToggleGroupItem = React.forwardRef<
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
size: context.size || size
}),
className
)}
Expand Down
9 changes: 6 additions & 3 deletions packages/e2e/src/app/app/transitions/client.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
'use client'

import { parseAsInteger, useQueryState } from 'nuqs'
import React from 'react'
import { useTransition } from 'react'
import { HydrationMarker } from '../../../components/hydration-marker'

export function Client() {
const [isLoading, startTransition] = React.useTransition()
const [isLoading, startTransition] = useTransition()
const [counter, setCounter] = useQueryState(
'counter',
parseAsInteger.withDefault(0).withOptions({ startTransition })
parseAsInteger.withDefault(0).withOptions({
shallow: false,
startTransition
})
)
return (
<>
Expand Down
3 changes: 2 additions & 1 deletion packages/e2e/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NuqsAdapter } from 'nuqs/adapters/next'
import React, { Suspense } from 'react'
import { HydrationMarker } from '../components/hydration-marker'

Expand All @@ -16,7 +17,7 @@ export default function RootLayout({
<Suspense>
<HydrationMarker />
</Suspense>
{children}
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
)
Expand Down
10 changes: 10 additions & 0 deletions packages/e2e/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { AppProps } from 'next/app'
import { NuqsAdapter } from 'nuqs/adapters/next'

export default function MyApp({ Component, pageProps }: AppProps) {
return (
<NuqsAdapter>
<Component {...pageProps} />
</NuqsAdapter>
)
}
7 changes: 7 additions & 0 deletions packages/nuqs/adapters/next.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// This file is needed for projects that have `moduleResolution` set to `node`
// in their tsconfig.json to be able to `import {} from 'nuqs/adpaters/next'`.
// Other module resolutions strategies will look for the `exports` in `package.json`,
// but with `node`, TypeScript will look for a .d.ts file with that name at the
// root of the package.

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

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

export * from '../dist/adapters/testing'
36 changes: 28 additions & 8 deletions packages/nuqs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
},
"files": [
"dist/",
"parsers.d.ts",
"server.d.ts"
"server.d.ts",
"adapters/react.d.ts",
"adapters/next.d.ts",
"adapters/testing.d.ts"
],
"type": "module",
"sideEffects": false,
Expand All @@ -43,11 +45,24 @@
"./server": {
"types": "./dist/server.d.ts",
"import": "./dist/server.js"
},
"./adapters/react": {
"types": "./dist/adapters/react.d.ts",
"import": "./dist/adapters/react.js"
},
"./adapters/next": {
"types": "./dist/adapters/next.d.ts",
"import": "./dist/adapters/next.js"
},
"./adapters/testing": {
"types": "./dist/adapters/testing.d.ts",
"import": "./dist/adapters/testing.js"
}
},
"scripts": {
"dev": "tsup --watch --external=react",
"build": "tsup --clean --external=react",
"dev": "tsup --watch",
"prebuild": "rm -rf dist",
"build": "tsup",
"postbuild": "size-limit --json > size.json",
"test": "pnpm run --parallel --stream '/^test:/'",
"test:types": "tsd",
Expand All @@ -56,17 +71,20 @@
"prepack": "./scripts/prepack.sh"
},
"peerDependencies": {
"next": ">= 14.1.2",
"react": ">= 18.2.0"
},
"dependencies": {
"mitt": "^3.0.1"
},
"optionalDependencies": {
"next": ">= 14.1.2"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.47.9",
"@size-limit/preset-small-lib": "^11.1.6",
"@types/node": "^22.7.5",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.11",
"next": "catalog:react19rc",
"react": "catalog:react19rc",
"react-dom": "catalog:react19rc",
Expand All @@ -86,15 +104,17 @@
"path": "dist/index.js",
"limit": "5 kB",
"ignore": [
"react"
"react",
"next"
]
},
{
"name": "Server",
"path": "dist/server.js",
"limit": "2 kB",
"ignore": [
"react"
"react",
"next"
]
}
]
Expand Down
16 changes: 16 additions & 0 deletions packages/nuqs/src/adapters/defs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Options } from '../defs'

export type AdapterOptions = Pick<Options, 'history' | 'scroll' | 'shallow'>

export type UpdateUrlFunction = (
search: URLSearchParams,
options: Required<AdapterOptions>
) => void

export type UseAdapterHook = () => AdapterInterface

export type AdapterInterface = {
searchParams: URLSearchParams
updateUrl: UpdateUrlFunction
rateLimitFactor?: number
}
31 changes: 31 additions & 0 deletions packages/nuqs/src/adapters/internal.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createContext, createElement, useContext, type ReactNode } from 'react'
import { error } from '../errors'
import type { UseAdapterHook } from './defs'

export type AdapterContext = {
useAdapter: UseAdapterHook
}

export const context = createContext<AdapterContext>({
useAdapter() {
throw new Error(error(404))
}
})
context.displayName = 'NuqsAdapterContext'

export function createAdapterProvider(useAdapter: UseAdapterHook) {
return ({ children, ...props }: { children: ReactNode }) =>
createElement(
context.Provider,
{ ...props, value: { useAdapter } },
children
)
}

export function useAdapter() {
const value = useContext(context)
if (!('useAdapter' in value)) {
throw new Error(error(404))
}
return value.useAdapter()
}
Loading

0 comments on commit f83f219

Please sign in to comment.