Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introducing adapters for other frameworks #644

Merged
merged 37 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b248711
feat: Introducing adapters for other frameworks
franky47 Sep 20, 2024
3137ce7
feat: Add vanilla React adapter sandbox
franky47 Sep 20, 2024
05134b3
doc: Add nuqs adapter
franky47 Sep 20, 2024
14ec0b6
feat: Provide specialised adapters for both Next.js routers
franky47 Sep 23, 2024
5e1a999
chore: Remove unused internals test
franky47 Sep 23, 2024
1970621
doc: Add adapters in v2 migration docs
franky47 Sep 24, 2024
9ea6252
feat: Add Remix adapter
franky47 Sep 25, 2024
5a095ab
chore: Move adapter to Remix root (wrapping Outlet)
franky47 Sep 26, 2024
970f318
doc: Wording
franky47 Sep 26, 2024
0751b92
feat: Add React Router adapter
franky47 Sep 26, 2024
36572a2
feat: Render pretty URLs in other adapters
franky47 Sep 27, 2024
319a1c1
chore: Use port 4001 for Remix
franky47 Sep 27, 2024
3dc9c10
chore: Order keys
franky47 Sep 27, 2024
43b0fa2
feat: Add adapter for One
franky47 Oct 5, 2024
d767279
chore: Reorder optionalDependency keys
franky47 Oct 5, 2024
c22cf92
chore: Fix Sherif warnings & errors
franky47 Oct 9, 2024
7dcc3ad
chore: Move support for One (onestack.dev) to community-based
franky47 Oct 10, 2024
10f2a7d
chore: Fix sherif
franky47 Oct 10, 2024
3339f91
chore: Add pre-push hook to check linter
franky47 Oct 10, 2024
00f78bd
chore: Update minimum supported Next.js version
franky47 Oct 11, 2024
c8256b1
chore: Reduce the test set for regular PRs
franky47 Oct 11, 2024
c3ebd69
chore: Don't specify basePath for intermediary versions
franky47 Oct 11, 2024
55de3e4
chore: Turns out if isn't supported in matrix.include
franky47 Oct 11, 2024
c191afa
feat: Add custom adapters API (unstable)
franky47 Oct 16, 2024
79fd842
doc: Update README with adapters docs
franky47 Oct 16, 2024
8285951
doc: Fix React Router logo in README
franky47 Oct 16, 2024
5aee930
doc: Light/dark mode doesn't seem to work in <img> tags
franky47 Oct 16, 2024
b0d4f91
doc: Update migration docs with adapters breaking changes
franky47 Oct 16, 2024
d577297
chore: Pretty print "use client" injection
franky47 Oct 16, 2024
fbedf37
chore: Update generated next .d.ts file
franky47 Oct 16, 2024
26ee606
doc: Wording & links
franky47 Oct 16, 2024
9557c07
test: Showcase usage of the NuqsTestingAdapter
franky47 Oct 21, 2024
2a3fa54
chore: Split tsconfig between type-checking & emitting at build
franky47 Oct 21, 2024
3a52dee
chore: Move urlKeys into the UseQueryStatesOptions type
franky47 Oct 21, 2024
45a7c90
doc: Update breaking changes in types
franky47 Oct 21, 2024
5be8e59
doc: Add adapters docs
franky47 Oct 21, 2024
35ce3f6
doc: Add section on testing
franky47 Oct 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,13 @@ jobs:
# branch protection settings for `next`.
base-path: [false, '/base']
next-version:
- '14.1.2'
- '14.1.3'
- '14.1.4'
# Only keep versions where there were relevant changes in the app router core,
# and the previous one to use as a baseline.
- '14.2.0'
- '14.2.1'
- '14.2.2'
- '14.2.3'
- '14.2.4'
- '14.2.5'
- '14.2.6'
- '14.2.7'
- '14.2.3' # before vercel/next.js#66755
- '14.2.4' # after vercel/next.js#66755
- '14.2.7' # before vercel/next.js#69509
- '14.2.8' # after vercel/next.js#69509
- latest
- local

Expand Down Expand Up @@ -89,7 +85,7 @@ jobs:
- name: Install dependencies
run: pnpm install --ignore-scripts --frozen-lockfile
- name: Check monorepo with Sherif
run: pnpm dlx sherif@1.0.0 --no-install
run: pnpm run lint:sherif
- name: Check source code formatting
run: |
set +e # Allow Prettier to fail, but capture the error code
Expand Down
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
215 changes: 165 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
# useQueryState for Next.js
# nuqs

[![NPM](https://img.shields.io/npm/v/nuqs?color=red)](https://www.npmjs.com/package/nuqs)
[![MIT License](https://img.shields.io/github/license/47ng/nuqs.svg?color=blue)](https://github.com/47ng/nuqs/blob/next/LICENSE)
[![Continuous Integration](https://github.com/47ng/nuqs/workflows/Continuous%20Integration/badge.svg?branch=next)](https://github.com/47ng/nuqs/actions)
[![Depfu](https://badges.depfu.com/badges/acad53fa2b09b1e435a19d6d18f29af4/count.svg)](https://depfu.com/github/47ng/nuqs?project_id=22104)

<!-- [![Coverage Status](https://coveralls.io/repos/github/47ng/nuqs/badge.svg?branch=next)](https://coveralls.io/github/47ng/nuqs?branch=next) -->

useQueryState hook for Next.js - Like React.useState, but stored in the URL query string
Type-safe search params state manager for React frameworks. Like `useState`, but stored in the URL query string.

## Features

- 🔀 Supports both the `app` and `pages` routers
- 🔀 **new:** Supports Next.js (`app` and `pages` routers), plain React (SPA), Remix, React Router, and custom routers via [adapters](#adapters)
- 🧘‍♀️ Simple: the URL is the source of truth
- 🕰 Replace history or [append](#history) to use the Back button to navigate state updates
- ⚡️ Built-in [parsers](#parsing) for common state types (integer, float, boolean, Date, and more)
- ⚡️ Built-in [parsers](#parsing) for common state types (integer, float, boolean, Date, and more). Create your own parsers for custom types & pretty URLs
- ♊️ Related querystrings with [`useQueryStates`](#usequerystates)
- 📡 [Shallow mode](#shallow) by default for URL query updates, opt-in to notify server components
- 🗃 _**new:**_ [Server cache](#accessing-searchparams-in-server-components) for type-safe searchParams access in nested server components
- ⌛️ _**new:**_ Support for [`useTransition`](#transitions) to get loading states on server updates
- 🗃 [Server cache](#accessing-searchparams-in-server-components) for type-safe searchParams access in nested server components
- ⌛️ Support for [`useTransition`](#transitions) to get loading states on server updates

## Documentation

Read the complete documentation at [nuqs.47ng.com](https://nuqs.47ng.com).

## Installation

Expand All @@ -34,21 +36,121 @@ yarn add nuqs
npm install nuqs
```

### Which version should I use?
## Adapters

You will need to wrap your React component tree with an adapter for your framework. _(expand the appropriate section below)_

<details><summary>▲ Next.js (app router)</summary>

| Next.js version range | Supported `nuqs` version |
| ----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- |
| >= 14.1.2 | `nuqs@latest` |
| >=14.0.4 && \<\= 14.1.1 | `nuqs@^1` |
| 14.0.3 | `nuqs@^1`, with the `windowHistorySupport` experimental flag, see [#417](https://github.com/47ng/nuqs/issues/417) |
| 14.0.2 | Not compatible, see issue [#388](https://github.com/47ng/nuqs/issues/388) and Next.js PR [#58297](https://github.com/vercel/next.js/pull/58297) |
| >= 13.1 && \<\= 14.0.1 | `nuqs@^1` |
| < 13.1 | `next-usequerystate@1.7.3` |
> Supported Next.js versions: `>=14.2.0`. For older versions, install `nuqs@^1` (which doesn't need this adapter code).

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

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

</details>

<details><summary>▲ Next.js (pages router)</summary>

> Supported Next.js versions: `>=14.2.0`. For older versions, install `nuqs@^1` (which doesn't need this adapter code).

```tsx
// src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { NuqsAdapter } from 'nuqs/adapters/next/pages'

export default function MyApp({ Component, pageProps }: AppProps) {
return (
<NuqsAdapter>
<Component {...pageProps} />
</NuqsAdapter>
)
}
```

</details>

<details><summary>⚛️ Plain React (SPA)</summary>

Example: via Vite or create-react-app.

```tsx
import { NuqsAdapter } from 'nuqs/adapters/react'

createRoot(document.getElementById('root')!).render(
<NuqsAdapter>
<App />
</NuqsAdapter>
)
```

</details>

<details><summary>💿 Remix</summary>

> Supported Remix versions: `@remix-run/react@>=2`

```tsx
// app/root.tsx
import { NuqsAdapter } from 'nuqs/adapters/remix'

// ...

export default function App() {
return (
<NuqsAdapter>
<Outlet />
</NuqsAdapter>
)
}
```

</details>

<details><summary><img style="width:1em;height:1em;" src="https://reactrouter.com/_brand/React%20Router%20Brand%20Assets/React%20Router%20Logo/Dark.svg" /> React Router
</summary>

> Supported React Router versions: `react-router-dom@>=6`

```tsx
import { NuqsAdapter } from 'nuqs/adapters/react-router'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import App from './App'

const router = createBrowserRouter([
{
path: '/',
element: <App />
}
])

export function ReactRouter() {
return (
<NuqsAdapter>
<RouterProvider router={router} />
</NuqsAdapter>
)
}
```

</details>

## Usage

```tsx
'use client' // app router: only works in client components
'use client' // Only works in client components

import { useQueryState } from 'nuqs'

Expand All @@ -66,8 +168,6 @@ export default () => {

![](https://raw.githubusercontent.com/47ng/nuqs/next/useQueryState.gif)

## Documentation

`useQueryState` takes one required argument: the key to use in the query string.

Like `React.useState`, it returns an array with the value present in the query
Expand Down Expand Up @@ -276,6 +376,8 @@ setQuery(null, { history: 'replace' })

### Shallow

> Note: this feature only applies to Next.js

By default, query state updates are done in a _client-first_ manner: there are
no network calls to the server.

Expand All @@ -294,21 +396,6 @@ const [state, setState] = useQueryState('foo', { shallow: false })
setState('bar', { shallow: false })
```

### Scroll

The Next.js router scrolls to the top of the page on navigation updates,
which may not be desirable when updating the query string with local state.

Query state updates won't scroll to the top of the page by default, but you
can opt-in to this behaviour (which was the default up to 1.8.0):

```ts
const [state, setState] = useQueryState('foo', { scroll: true })

// You can also pass the option on calls to setState:
setState('bar', { scroll: true })
```

### Throttling URL updates

Because of browsers rate-limiting the History API, internal updates to the
Expand Down Expand Up @@ -345,7 +432,7 @@ 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` automatically for you)_:
to enable this behaviour:

```tsx
'use client'
Expand All @@ -359,7 +446,10 @@ function ClientComponent({ data }) {
const [query, setQuery] = useQueryState(
'query',
// 2. Pass the `startTransition` as an option:
parseAsString().withOptions({ startTransition })
parseAsString().withOptions({
startTransition,
shallow: false // opt-in to notify the server (Next.js only)
})
)
// 3. `isLoading` will be true while the server is re-rendering
// and streaming RSC payloads, when the query is updated via `setQuery`.
Expand Down Expand Up @@ -713,14 +803,46 @@ inferParserType<typeof parsers>

## Testing

Currently, the best way to test the behaviour of your components using
`useQueryState(s)` is end-to-end testing, with tools like Playwright or Cypress.
Since nuqs v2, you can use a testing adapter to unit-test components using
`useQueryState` and `useQueryStates` in isolation, without needing to mock
your framework or router.

Here's an example using Testing Library and Vitest:

Running components that use the Next.js router in isolation requires mocking it,
which is being [worked on](https://github.com/scottrippey/next-router-mock/pull/103)
for the app router.
```tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'

it('should increment the count when clicked', async () => {
const user = userEvent.setup()
const onUrlUpdate = vi.fn<[UrlUpdateEvent]>()
render(<CounterButton />, {
// Setup the test by passing initial search params / querystring,
// and give it a function to call on URL updates
wrapper: ({ children }) => (
<NuqsTestingAdapter searchParams="?count=42" onUrlUpdate={onUrlUpdate}>
{children}
</NuqsTestingAdapter>
)
})
// Initial state assertions: there's a clickable button displaying the count
const button = screen.getByRole('button')
expect(button).toHaveTextContent('count is 42')
// Act
await user.click(button)
// Assert changes in the state and in the (mocked) URL
expect(button).toHaveTextContent('count is 43')
expect(onUrlUpdate).toHaveBeenCalledOnce()
expect(onUrlUpdate.mock.calls[0][0].queryString).toBe('?count=43')
expect(onUrlUpdate.mock.calls[0][0].searchParams.get('count')).toBe('43')
expect(onUrlUpdate.mock.calls[0][0].options.history).toBe('push')
})
```

See issue #259 for more testing-related discussions.
See [#259](https://github.com/47ng/nuqs/issues/259) for more testing-related discussions.

## Debugging

Expand All @@ -744,13 +866,6 @@ your browser's devtools.
Providing debug logs when opening an [issue](https://github.com/47ng/nuqs/issues)
is always appreciated. 🙏

## Caveats

Because the Next.js **pages router** is not available in an SSR context, this
hook will always return `null` (or the default value if supplied) on SSR/SSG.

This limitation doesn't apply to the app router.

### SEO

If your page uses query strings for local-only state, you should add a
Expand Down
28 changes: 28 additions & 0 deletions errors/NUQS-404.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# `nuqs` requires an adapter to work with your framework

## Probable cause

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

[Adapters](https://nuqs.47ng.com/docs/adapters) are based on React Context,
and provide nuqs hooks with the interfaces to work with your framework:
reacting to URL changes, and calling your router when you update your state.

## Possible solutions

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

- [Next.js (app router)](https://nuqs.47ng.com/docs/adapters#nextjs-app-router)
- [Next.js (pages router)](https://nuqs.47ng.com/docs/adapters#nextjs-pages-router)
- [React SPA (eg: with Vite)](https://nuqs.47ng.com/docs/adapters#react-spa)
- [Remix](https://nuqs.47ng.com/docs/adapters#remix)
- [React Router](https://nuqs.47ng.com/docs/adapters#react-router)

### Test adapter

If you encounter this error outside of the browser, like in a test
runner (eg: Vitest or Jest), you may use the [testing adapter](https://nuqs.47ng.com/docs/testing)
from `nuqs/adapters/testing` to mock the initial search params and access
setup/assertion testing facilities.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"prepare": "husky"
"prepare": "husky",
"lint": "pnpm run -w --parallel --stream '/^lint:/'",
"lint:prettier": "prettier --check ./packages/nuqs/src/**/*.ts",
"lint:sherif": "sherif"
},
"devDependencies": {
"@commitlint/config-conventional": "^19.5.0",
"commitlint": "^19.5.0",
"husky": "^9.1.6",
"prettier": "3.3.3",
"semantic-release": "^24.1.2",
"sherif": "^1.0.0",
"turbo": "^2.1.3",
"typescript": "^5.6.3"
},
Expand All @@ -38,7 +42,8 @@
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-push": "pnpm run lint"
}
},
"commitlint": {
Expand Down
Loading
Loading