From 56c223fd6492994f60ebb4d19ee9664ab2f8af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Best?= Date: Wed, 23 Oct 2024 10:13:08 +0200 Subject: [PATCH] fix: Make clearOnDefault: true by default (#700) --- .../react/src/components/search-input.tsx | 2 +- .../remix/app/components/search-input.tsx | 2 +- .../playground/(demos)/tic-tac-toe/client.tsx | 4 +- packages/e2e/cypress/e2e/clearOnDefault.cy.js | 4 +- .../e2e/src/app/app/clearOnDefault/page.tsx | 21 +++++----- .../e2e/src/app/app/remapped-keys/page.tsx | 1 - packages/e2e/src/app/app/template/page.tsx | 3 +- packages/nuqs/src/defs.ts | 4 +- packages/nuqs/src/serializer.test.ts | 39 +++++++++++++------ packages/nuqs/src/serializer.ts | 5 ++- packages/nuqs/src/useQueryState.ts | 4 +- packages/nuqs/src/useQueryStates.ts | 2 +- 12 files changed, 54 insertions(+), 37 deletions(-) diff --git a/packages/adapters/react/src/components/search-input.tsx b/packages/adapters/react/src/components/search-input.tsx index b6baf42c..438e8a7b 100644 --- a/packages/adapters/react/src/components/search-input.tsx +++ b/packages/adapters/react/src/components/search-input.tsx @@ -2,7 +2,7 @@ import { parseAsString, useQueryStates } from 'nuqs' export function SearchInput() { const [{ search }, setSearch] = useQueryStates({ - search: parseAsString.withDefault('').withOptions({ clearOnDefault: true }) + search: parseAsString.withDefault('') }) return ( ({ function useGameEngine() { const [{ board, status }, setGameState] = useQueryState( 'board', - gameParser - .withDefault(defaultState) - .withOptions({ clearOnDefault: true, history: 'push' }) + gameParser.withDefault(defaultState).withOptions({ history: 'push' }) ) const play = useCallback( (i: number, j: number) => { diff --git a/packages/e2e/cypress/e2e/clearOnDefault.cy.js b/packages/e2e/cypress/e2e/clearOnDefault.cy.js index 42097ef2..942322b8 100644 --- a/packages/e2e/cypress/e2e/clearOnDefault.cy.js +++ b/packages/e2e/cypress/e2e/clearOnDefault.cy.js @@ -2,9 +2,9 @@ it('Clears the URL when setting the default value when `clearOnDefault` is used', () => { cy.visit( - '/app/clearOnDefault?a=a&b=b&array=1,2,3&json-ref={"egg":"spam"}&json-new={"egg":"spam"}' + '/app/clearOnDefault?a=a&b=b&array=1,2,3&json-ref={"egg":"spam"}&json-new={"egg":"spam"}&keepMe=init' ) cy.contains('#hydration-marker', 'hydrated').should('be.hidden') cy.get('button').click() - cy.location('search').should('eq', '?a=') + cy.location('search').should('eq', '?a=&keepMe=') }) diff --git a/packages/e2e/src/app/app/clearOnDefault/page.tsx b/packages/e2e/src/app/app/clearOnDefault/page.tsx index 3a268e02..42f5df88 100644 --- a/packages/e2e/src/app/app/clearOnDefault/page.tsx +++ b/packages/e2e/src/app/app/clearOnDefault/page.tsx @@ -4,6 +4,7 @@ import { parseAsArrayOf, parseAsInteger, parseAsJson, + parseAsString, useQueryState } from 'nuqs' import { Suspense } from 'react' @@ -22,26 +23,23 @@ const runtimePassthrough = (x: unknown) => x function Client() { const [, setA] = useQueryState('a') const [, setB] = useQueryState('b', { - defaultValue: '', - clearOnDefault: true + defaultValue: '' }) const [, setArray] = useQueryState( 'array', - parseAsArrayOf(parseAsInteger) - .withDefault([]) - .withOptions({ clearOnDefault: true }) + parseAsArrayOf(parseAsInteger).withDefault([]) ) const [, setJsonRef] = useQueryState( 'json-ref', - parseAsJson(runtimePassthrough) - .withDefault(defaultJSON) - .withOptions({ clearOnDefault: true }) + parseAsJson(runtimePassthrough).withDefault(defaultJSON) ) const [, setJsonNew] = useQueryState( 'json-new', - parseAsJson(runtimePassthrough) - .withDefault(defaultJSON) - .withOptions({ clearOnDefault: true }) + parseAsJson(runtimePassthrough).withDefault(defaultJSON) + ) + const [, keepMe] = useQueryState( + 'keepMe', + parseAsString.withDefault('').withOptions({ clearOnDefault: false }) ) return ( <> @@ -52,6 +50,7 @@ function Client() { setArray([]) setJsonRef(defaultJSON) setJsonNew({ ...defaultJSON }) + keepMe('') }} > Clear diff --git a/packages/e2e/src/app/app/remapped-keys/page.tsx b/packages/e2e/src/app/app/remapped-keys/page.tsx index b14c5082..27b67c6a 100644 --- a/packages/e2e/src/app/app/remapped-keys/page.tsx +++ b/packages/e2e/src/app/app/remapped-keys/page.tsx @@ -24,7 +24,6 @@ function Client() { activeTags: parseAsArrayOf(parseAsString).withDefault([]) }, { - clearOnDefault: true, urlKeys: { searchQuery: 'q', pageNumber: 'page', diff --git a/packages/e2e/src/app/app/template/page.tsx b/packages/e2e/src/app/app/template/page.tsx index d980703b..23609b34 100644 --- a/packages/e2e/src/app/app/template/page.tsx +++ b/packages/e2e/src/app/app/template/page.tsx @@ -13,8 +13,7 @@ export default function Page() { function Client() { const [state, setState] = useQueryState('state', { - defaultValue: '', - clearOnDefault: true + defaultValue: '' }) return ( <> diff --git a/packages/nuqs/src/defs.ts b/packages/nuqs/src/defs.ts index 1afdf32e..49fe764b 100644 --- a/packages/nuqs/src/defs.ts +++ b/packages/nuqs/src/defs.ts @@ -54,7 +54,9 @@ export type Options = { * Clear the key-value pair from the URL query string when setting the state * to the default value. * - * Defaults to `false` to keep backwards-compatiblity when the default value + * Defaults to `true` to keep URLs clean. + * + * Set it to `false` to keep backwards-compatiblity when the default value * changes (prefer explicit URLs whose meaning don't change). */ clearOnDefault?: boolean diff --git a/packages/nuqs/src/serializer.test.ts b/packages/nuqs/src/serializer.test.ts index 561c183f..b147af6e 100644 --- a/packages/nuqs/src/serializer.test.ts +++ b/packages/nuqs/src/serializer.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'vitest' +import type { Options } from './defs' import { parseAsArrayOf, parseAsBoolean, @@ -85,18 +86,32 @@ describe('serializer', () => { const result = serialize('?str=foo&external=kept', null) expect(result).toBe('?external=kept') }) - test('clears value when setting the default value when `clearOnDefault` is used', () => { + test('clears value when setting the default value (`clearOnDefault: true` is the default)', () => { const serialize = createSerializer({ - int: parseAsInteger.withOptions({ clearOnDefault: true }).withDefault(0), - str: parseAsString.withOptions({ clearOnDefault: true }).withDefault(''), - bool: parseAsBoolean - .withOptions({ clearOnDefault: true }) - .withDefault(false), - arr: parseAsArrayOf(parseAsString) - .withOptions({ clearOnDefault: true }) - .withDefault([]), + int: parseAsInteger.withDefault(0), + str: parseAsString.withDefault(''), + bool: parseAsBoolean.withDefault(false), + arr: parseAsArrayOf(parseAsString).withDefault([]), + json: parseAsJson(x => x).withDefault({ foo: 'bar' }) + }) + const result = serialize({ + int: 0, + str: '', + bool: false, + arr: [], + json: { foo: 'bar' } + }) + expect(result).toBe('') + }) + test('keeps value when setting the default value when `clearOnDefault: false`', () => { + const options: Options = { clearOnDefault: false } + const serialize = createSerializer({ + int: parseAsInteger.withOptions(options).withDefault(0), + str: parseAsString.withOptions(options).withDefault(''), + bool: parseAsBoolean.withOptions(options).withDefault(false), + arr: parseAsArrayOf(parseAsString).withOptions(options).withDefault([]), json: parseAsJson(x => x) - .withOptions({ clearOnDefault: true }) + .withOptions(options) .withDefault({ foo: 'bar' }) }) const result = serialize({ @@ -106,6 +121,8 @@ describe('serializer', () => { arr: [], json: { foo: 'bar' } }) - expect(result).toBe('') + expect(result).toBe( + '?int=0&str=&bool=false&arr=&json={%22foo%22:%22bar%22}' + ) }) }) diff --git a/packages/nuqs/src/serializer.ts b/packages/nuqs/src/serializer.ts index 7e3f147d..a97c3cb5 100644 --- a/packages/nuqs/src/serializer.ts +++ b/packages/nuqs/src/serializer.ts @@ -52,7 +52,10 @@ export function createSerializer< parser.defaultValue !== undefined && (parser.eq ?? ((a, b) => a === b))(value, parser.defaultValue) - if (value === null || (parser.clearOnDefault && isMatchingDefault)) { + if ( + value === null || + ((parser.clearOnDefault ?? true) && isMatchingDefault) + ) { search.delete(key) } else { search.set(key, parser.serialize(value)) diff --git a/packages/nuqs/src/useQueryState.ts b/packages/nuqs/src/useQueryState.ts index 42b5f73b..58febcd2 100644 --- a/packages/nuqs/src/useQueryState.ts +++ b/packages/nuqs/src/useQueryState.ts @@ -211,7 +211,7 @@ export function useQueryState( serialize = String, eq = (a, b) => a === b, defaultValue = undefined, - clearOnDefault = false, + clearOnDefault = true, startTransition }: Partial> & { defaultValue?: T @@ -223,7 +223,7 @@ export function useQueryState( parse: x => x as unknown as T, serialize: String, eq: (a, b) => a === b, - clearOnDefault: false, + clearOnDefault: true, defaultValue: undefined } ) { diff --git a/packages/nuqs/src/useQueryStates.ts b/packages/nuqs/src/useQueryStates.ts index 2e1fc6df..0b640a35 100644 --- a/packages/nuqs/src/useQueryStates.ts +++ b/packages/nuqs/src/useQueryStates.ts @@ -74,7 +74,7 @@ export function useQueryStates( scroll = false, shallow = true, throttleMs = FLUSH_RATE_LIMIT_MS, - clearOnDefault = false, + clearOnDefault = true, startTransition, urlKeys = defaultUrlKeys }: Partial> = {}