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: Sort query string keys #638

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
bfd846a
chore: ESM only (drop CJS support)
franky47 Jan 15, 2024
b23e07a
chore: Remove deprecated APIs
franky47 Jan 15, 2024
0fa74ff
chore: Drop mirrorring to next-usequerystate
franky47 Jan 15, 2024
bd1cf58
chore: Rename `nuqs/parsers` to `nuqs/server`
franky47 Jan 15, 2024
1795d2e
chore: Fix path to server entrypoint
franky47 Jan 15, 2024
c920e08
chore: Add peer dep on React (for `cache`)
franky47 Jan 15, 2024
c718b0e
doc: Add v2 migration docs
franky47 Jan 16, 2024
541ebef
doc: Formatting
franky47 Jan 16, 2024
fadd38c
doc: Fix ESM/CJS error
franky47 Jan 16, 2024
8f2dd30
chore: Drop unnecessary ESM specifier for dev
franky47 Jan 17, 2024
51a1ae7
chore: Update list of supported versions
franky47 Jan 17, 2024
0996b91
doc: Wording
franky47 Jan 17, 2024
87b7257
chore: Next GA is 14.1.0
franky47 Jan 18, 2024
cfcea0d
chore: Query spy with reactive useSearchParams
franky47 Feb 5, 2024
51e8c4f
doc: Query spy follows encoding
franky47 Feb 5, 2024
734e6e5
test: Remove support for unstable shallow routing
franky47 Feb 17, 2024
36b21d8
chore: Peer dep on fixed version for #498
franky47 Mar 26, 2024
a6f2207
chore: Update Next.js version range to test against
franky47 Mar 26, 2024
9e9d3b4
test: Test v2 against the React Compiler
franky47 Jun 27, 2024
e6c94ed
chore: Don't define the compiler flag when false
franky47 Jun 28, 2024
dc97d2c
chore: Fix config access
franky47 Jun 28, 2024
39dd6be
chore: Only support next@14.2.0 and above
franky47 Jul 26, 2024
c556883
chore: Fix linter by sorting devDeps alphabetically
franky47 Sep 6, 2024
fa0b732
chore: Test all supported next@^14 versions
franky47 Sep 6, 2024
9c93ff2
chore: Replace history patching with uSP sync
franky47 Sep 6, 2024
ca9971d
chore: Remove notification types & exports
franky47 Sep 6, 2024
d31ff4e
chore: Remove exports
franky47 Sep 6, 2024
0f012c3
chore: No more side-effects
franky47 Sep 6, 2024
9e54b47
chore: Remove unused sync code
franky47 Sep 7, 2024
97a1ea8
ref: Pure internal state init function (#633)
franky47 Sep 9, 2024
330df9b
feat: Sort search params keys
hugotiger Sep 16, 2024
2cae1ae
test: Fix failing tests
hugotiger Sep 16, 2024
71c84cc
doc: Add note about sorting of query string keys
hugotiger Sep 16, 2024
bc05ce5
test: Update failing tests
hugotiger Sep 16, 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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Are you using:
- ✅/❌ The app router
- ✅/❌ The pages router
- ✅/❌ The `basePath` option in your Next.js config
- ✅/❌ The experimental `windowHistorySupport` flag in your Next.js config
- ✅/❌ The experimental `windowHistorySupport` flag in your Next.js config _(only relevant for Next.js 14.0.3 or 14.0.4)_

## Description

Expand Down
39 changes: 15 additions & 24 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
# Watch out! When changing the job name,
# update the required checks in GitHub
# branch protection settings for `next`.
name: CI (${{ matrix.next-version }}${{ matrix.base-path && ' basePath' || ''}}${{ matrix.window-history-support && ' WHS' || ''}})
name: CI (${{ matrix.next-version }}${{ matrix.base-path && ' basePath' || ''}})
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -22,21 +22,19 @@ jobs:
# update the required checks in GitHub
# branch protection settings for `next`.
base-path: [false, '/base']
window-history-support: [false]
next-version:
- '13.4'
- '13.5'
- '14.0.1'
# 14.0.2 is not compatible due to a prefetch issue
# 14.0.3 requires the WHS flag (see below)
- '14.0.4'
- latest # Current latest is 14.1.0
include:
- next-version: '14.0.3'
window-history-support: true
# 14.0.4 doesn't require the WHS flag, but supports it
- next-version: '14.0.4'
window-history-support: true
- '14.1.2'
- '14.1.3'
- '14.1.4'
- '14.2.0'
- '14.2.1'
- '14.2.2'
- '14.2.3'
- '14.2.4'
- '14.2.5'
- '14.2.6'
- '14.2.7'
- latest

steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
Expand All @@ -53,7 +51,6 @@ jobs:
run: pnpm run test
env:
BASE_PATH: ${{ matrix.base-path && matrix.base-path || '/' }}
WINDOW_HISTORY_SUPPORT: ${{ matrix.window-history-support }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
E2E_NO_CACHE_ON_RERUN: ${{ github.run_attempt }}
Expand All @@ -62,13 +59,13 @@ jobs:
if: failure()
with:
path: packages/e2e/cypress/screenshots
name: ci-${{ matrix.next-version }}${{ matrix.base-path && '-basePath' || ''}}${{ matrix.window-history-support && '-whs' || ''}}
name: ci-${{ matrix.next-version }}${{ matrix.base-path && '-basePath' || ''}}
- uses: 47ng/actions-slack-notify@main
name: Notify on Slack
if: always()
with:
status: ${{ job.status }}
jobName: next@${{ matrix.next-version }}${{ matrix.base-path && ' basePath' || ''}}${{ matrix.window-history-support && ' WHS' || ''}}
jobName: next@${{ matrix.next-version }}${{ matrix.base-path && ' basePath' || ''}}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Expand Down Expand Up @@ -125,11 +122,5 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Mirror to next-usequerystate
run: ./scripts/mirror.sh
working-directory: packages/nuqs
continue-on-error: true
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Invalidate ISR cache for NPM in the docs
run: curl -s "https://nuqs.47ng.com/api/isr?tag=npm&token=${{ secrets.ISR_TOKEN }}"
8 changes: 5 additions & 3 deletions .github/workflows/test-against-nextjs-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ env:

jobs:
test_against_nextjs_release:
name: CI (${{ inputs.version }}${{ matrix.base-path && ' basePath' || ''}})
name: CI (${{ inputs.version }}${{ matrix.base-path && ' basePath' || ''}}${{ matrix.react-compiler && ' ⚛️⚡️' || ''}})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
base-path: [false, '/base']
react-compiler: [true, false]
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
Expand All @@ -35,6 +36,7 @@ jobs:
run: pnpm run test
env:
BASE_PATH: ${{ matrix.base-path && matrix.base-path || '/' }}
REACT_COMPILER: ${{ matrix.react-compiler }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
E2E_NO_CACHE_ON_RERUN: ${{ github.run_attempt }}
Expand All @@ -43,13 +45,13 @@ jobs:
if: failure()
with:
path: packages/e2e/cypress/screenshots
name: ci-${{ inputs.version }}${{ matrix.base-path && '-basePath' || ''}}
name: ci-${{ inputs.version }}${{ matrix.base-path && '-basePath' || ''}}${{ matrix.react-compiler && '-react-compiler' || ''}}
- uses: 47ng/actions-slack-notify@main
name: Notify on Slack
if: always()
with:
status: ${{ job.status }}
jobName: next@${{ inputs.version }}${{ matrix.base-path && ' basePath' || ''}}
jobName: next@${{ inputs.version }}${{ matrix.base-path && ' basePath' || ''}}${{ matrix.react-compiler && ' ⚛️⚡️' || ''}}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/content/docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ bun add nuqs
<Callout title={<>What happened to `next-usequerystate`?</>}>
It was a mouthful to type, so I decided to abbreviate it to `nuqs`.

The `nuqs` name was introduced in 1.14.0, and `next-usequerystate` will mirror
The `nuqs` name was introduced in 1.14.0, and `next-usequerystate` mirrored
its versions for the rest of the 1.x.x range. The next major version update
(2.0.0) and subsequent versions will only be published under the `nuqs` name.
</Callout>
87 changes: 87 additions & 0 deletions packages/docs/content/docs/migrations/v2.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
title: Migration guide to v2
description: How to update your code to use nuqs@2.0.0
---

## Dropped support for `next@14.0.3`

It may seem weird to drop support for a single patch version, and keep it for
older versions, but this is due to a bug in shallow routing in Next.js 14.0.3
that was fixed in 14.0.4, and that became hard to work around without ugly hacks
as Next.js releases evolved.

See #423 for context and a table of supported versions.

## ESM only

`nuqs@2.0.0` is now an [ESM-only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
package. This should not be much of an issue since
Next.js supports ESM in app code since version 12, but if you are bundling
`nuqs` code into an intermediate CJS library to be consumed in Next.js,
you'll run into import issues:

```txt
[ERR_REQUIRE_ESM]: require() of ES Module not supported
```

If converting your library to ESM is not possible, your main option is to
dynamically import `nuqs`:

```ts
const { useQueryState } = await import('nuqs')
```

## Deprecated exports

Some of the v1 API was marked as deprecated back in September 2023, and has been
removed in `nuqs@2.0.0`.

### `queryTypes` parsers object

The `queryTypes` object has been removed in favor of individual parser exports,
for better tree-shaking.

Replace with `parseAsXYZ` to match:

```diff
- import { queryTypes } from 'nuqs'
+ import { parseAsString, parseAsInteger, ... } from 'nuqs'

- useQueryState('q', queryTypes.string.withOptions({ ... }))
- useQueryState('page', queryTypes.integer.withDefault(1))
+ useQueryState('q', parseAsString.withOptions({ ... }))
+ useQueryState('page', parseAsInteger.withDefault(1))
```

### `subscribeToQueryUpdates`

Next.js 14.1.0 makes `useSearchParams` reactive to shallow search params updates,
which makes this internal helper function redundant. See #425 for context.

## Renamed `nuqs/parsers` to `nuqs/server`

When introducing the server cache in #387, the dedicated export for parsers was
reused as it didn't include the `"use client"` directive. Since it now contains
more than parsers and probably will be extended with server-only code in the future,
it has been renamed to a clearer export name.

Find and replace all occurrences of `nuqs/parsers` to `nuqs/server` in your code:

```diff
- import { parseAsInteger, createSearchParamsCache } from 'nuqs/parsers'
+ import { parseAsInteger, createSearchParamsCache } from 'nuqs/server'
```

## Debug printout detection

After the rename to `nuqs`, the debugging printout detection logic handled either
`next-usequerystate` or `nuqs` being present in the `localStorage.debug` variable.
`nuqs@2.0.0` only checks for the presence of the `nuqs` substring to enable logs.

Update your local dev environments to match by running this once in the devtools console:

```ts
if (localStorage.debug) {
localStorage.debug = localStorage.debug.replace('next-usequerystate', 'nuqs')
}
```
7 changes: 7 additions & 0 deletions packages/docs/content/docs/seo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ export async function generateMetadata({
}
}
```

By default, query strings are sorted by their keys to prevent SEO issues related to variations in query string order.
The following example will serialize the query string as `?apple=foo&banana=foo`:

```tsx
useQueryState({ banana: 'foo', apple: 'foo' })
```
3 changes: 2 additions & 1 deletion packages/docs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { withSentryConfig } from '@sentry/nextjs'
import createNextDocsMDX from 'next-docs-mdx/config'
import remarkGitHub from 'remark-github'
import remarkMdxImages from 'remark-mdx-images'
import remarkSmartypants from 'remark-smartypants'

const withFumaMDX = createNextDocsMDX({
mdxOptions: {
remarkPlugins: [remarkMdxImages, remarkSmartypants]
remarkPlugins: [remarkMdxImages, remarkGitHub, remarkSmartypants]
}
})

Expand Down
11 changes: 11 additions & 0 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
"version": "0.0.0-internal",
"private": true,
"type": "module",
"author": {
"name": "François Best",
"email": "contact@francoisbest.com",
"url": "https://francoisbest.com"
},
"repository": {
"type": "git",
"url": "git+https://github.com/47ng/nuqs.git",
"directory": "packages/docs"
},
"scripts": {
"dev": "next dev",
"build": "next build",
Expand Down Expand Up @@ -37,6 +47,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"recharts": "^2.12.7",
"remark-github": "^12.0.0",
"remark-smartypants": "^2.1.0",
"res": "workspace:*",
"semver": "^7.6.3",
Expand Down
1 change: 0 additions & 1 deletion packages/docs/src/app/(pages)/_landing/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export async function LandingDemo() {
!line.includes('className="') && !line.includes('data-interacted=')
)
.join('\n')
.replaceAll('next-usequerystate', 'nuqs')
return (
<>
<Suspense
Expand Down
50 changes: 25 additions & 25 deletions packages/docs/src/app/playground/(demos)/_components/query-spy.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
'use client'

import { useSearchParams } from 'next/navigation'
import { subscribeToQueryUpdates } from 'nuqs'
import { createSerializer } from 'nuqs/server'
import React from 'react'
import { QuerySpySkeleton } from './query-spy.skeleton'

export function QuerySpy(props: React.ComponentProps<'pre'>) {
const initialSearchParams = useSearchParams()
const [search, setSearch] = React.useState<URLSearchParams>(() => {
if (typeof location !== 'object') {
// SSR
const out = new URLSearchParams()
if (!initialSearchParams) {
return out
}
for (const [key, value] of initialSearchParams) {
out.set(key, value)
}
return out
} else {
return new URLSearchParams(location.search)
}
})
const serialize = createSerializer({})

React.useLayoutEffect(
() => subscribeToQueryUpdates(({ search }) => setSearch(search)),
[]
export function QuerySpy(props: React.ComponentProps<'pre'>) {
useSearchParams() // Just using it to trigger re-render on query change
const searchParams = parseQuery(
serialize(new URLSearchParams(location.search), {}).slice(1) // Remove leading '?'
)

return (
<QuerySpySkeleton {...props}>
{search.size > 0 && (
{searchParams.length > 0 && (
<span className="text-zinc-500">
?
{Array.from(search.entries()).map(([key, value], i) => (
{searchParams.map(([key, value], i) => (
<React.Fragment key={key + i}>
<span className="text-[#005CC5] dark:text-[#79B8FF]">{key}</span>=
<span className="text-[#D73A49] dark:text-[#F97583]">
{value}
</span>
{i < search.size - 1 && <span className="text-zinc-500">&</span>}
{i < searchParams.length - 1 && (
<span className="text-zinc-500">&</span>
)}
</React.Fragment>
))}
</span>
)}
{search.size === 0 && (
{searchParams.length === 0 && (
<span className="italic text-zinc-500">{'<empty query>'}</span>
)}
</QuerySpySkeleton>
)
}

function parseQuery(queryString: string): [string, string][] {
const elements = queryString.split('&')
if (elements.length === 0) return []
return elements.reduce(
(acc, element) => {
if (element === '') return acc
const [key, value] = element.split('=')
return [...acc, [key, value]]
},
[] as [string, string][]
)
}

This file was deleted.

Loading