From 756d23bc8445609981fc4f85eafec881db476904 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 13 Mar 2025 22:09:36 +0530 Subject: [PATCH 1/2] feat: add `historyReplace` option to `setQueryString` of `useQueryParams` --- docs/api/useQueryParams.md | 34 +++++++++++++++++++++++++++++----- src/querystring.ts | 5 +++-- test/querystring.spec.tsx | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/docs/api/useQueryParams.md b/docs/api/useQueryParams.md index 1fb73d0..c7793c3 100644 --- a/docs/api/useQueryParams.md +++ b/docs/api/useQueryParams.md @@ -12,10 +12,15 @@ A hook for reading and updating the query string parameters on the page. Updates ```typescript -export function useQueryParams( - parseFn?: (query: string) => QueryParam, - serializeFn?: (query: QueryParam) => string -): [QueryParam, (query: QueryParam, replace?: boolean) => void] +export function useQueryParams( + parseFn?: (query: string) => T, + serializeFn?: (query: Partial) => string +): [T, (query: T, options?: setQueryParamsOptions) => void] + +export interface setQueryParamsOptions { + replace?: boolean + historyReplace?: boolean +} ``` ## Basic @@ -44,7 +49,7 @@ function UserList ({ users }) { ## Updating the Query with merge -The second return value from `useQueryParams` is a function that updates the query string. By default it overwrites the entire query, but it can merge with the query object by setting the second param to `{ replace: false }`. +The second return value from `useQueryParams` is a function that updates the query string. By default it overwrites the entire query, but it can merge with the query object by setting the `replace` option to `false`. ```jsx import { useQueryParams } from 'raviger' @@ -59,6 +64,25 @@ function UserList ({ users }) { The `replace: false` setting also preserves the `location.hash`. The intent should be thought of as updating only the part of the URL that the `setQuery` object describes. +You can also control whether the navigation replaces the current history entry by using the `historyReplace` option: + +```jsx +import { useQueryParams } from 'raviger' + +function UserList ({ users }) { + const [{ startsWith }, setQuery] = useQueryParams() + + // This will update the query params without adding a new history entry + const handleChange = (e) => { + setQuery({ startsWith: e.target.value}, { historyReplace: true }) + } + + return ( + + ) +} +``` + > Warning: using `setQuery` inside of a `useEffect` (or other on-mount/on-update lifecycle methods) can result in unwanted navigations, which show up as duplicate entries in the browser history stack. ## Custom serialization and parsing diff --git a/src/querystring.ts b/src/querystring.ts index 374ab6e..7d106ec 100644 --- a/src/querystring.ts +++ b/src/querystring.ts @@ -11,6 +11,7 @@ export interface QueryParam { export interface setQueryParamsOptions { replace?: boolean + historyReplace?: boolean } export function useQueryParams( @@ -19,7 +20,7 @@ export function useQueryParams( ): [T, (query: T, options?: setQueryParamsOptions) => void] { const [querystring, setQuerystring] = useState(getQueryString()) const setQueryParams = useCallback( - (params, { replace = true } = {}) => { + (params, { replace = true, historyReplace = false } = {}) => { let path = getCurrentPath() params = replace ? params : { ...parseFn(querystring), ...params } const serialized = serializeFn(params).toString() @@ -27,7 +28,7 @@ export function useQueryParams( if (serialized) path += '?' + serialized if (!replace) path += getCurrentHash() - navigate(path) + navigate(path, { replace: historyReplace }) }, [querystring, parseFn, serializeFn] ) diff --git a/test/querystring.spec.tsx b/test/querystring.spec.tsx index bce283f..754edc6 100644 --- a/test/querystring.spec.tsx +++ b/test/querystring.spec.tsx @@ -32,10 +32,18 @@ describe('useQueryParams', () => { }) describe('setQueryParams', () => { - function Route({ replace, foo = 'bar' }: { replace?: boolean; foo?: string | null }) { + function Route({ + replace, + historyReplace, + foo = 'bar', + }: { + replace?: boolean + historyReplace?: boolean + foo?: string | null + }) { const [query, setQuery] = useQueryParams() return ( - ) @@ -83,4 +91,26 @@ describe('setQueryParams', () => { act(() => void fireEvent.click(getByTestId('update'))) expect(document.location.hash).toEqual('#test') }) + + test('uses history.replaceState when historyReplace is true', async () => { + const replaceStateSpy = jest.spyOn(window.history, 'replaceState') + replaceStateSpy.mockClear() + act(() => navigate('/about', { query: { bar: 'foo' } })) + const { getByTestId } = render() + act(() => void fireEvent.click(getByTestId('update'))) + expect(replaceStateSpy).toHaveBeenCalled() + expect(document.location.search).toEqual('?foo=bar') + replaceStateSpy.mockRestore() + }) + + test('uses history.pushState when historyReplace is false', async () => { + const pushStateSpy = jest.spyOn(window.history, 'pushState') + pushStateSpy.mockClear() + act(() => navigate('/about', { query: { bar: 'foo' } })) + const { getByTestId } = render() + act(() => void fireEvent.click(getByTestId('update'))) + expect(pushStateSpy).toHaveBeenCalled() + expect(document.location.search).toEqual('?foo=bar') + pushStateSpy.mockRestore() + }) }) From b0b7722c36b327f4fd4a252a3e02a8f1e32a7483 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 13 Mar 2025 22:36:31 +0530 Subject: [PATCH 2/2] Update useQueryParams docs: inline options type definition --- docs/api/useQueryParams.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/api/useQueryParams.md b/docs/api/useQueryParams.md index c7793c3..c1e5364 100644 --- a/docs/api/useQueryParams.md +++ b/docs/api/useQueryParams.md @@ -15,12 +15,7 @@ A hook for reading and updating the query string parameters on the page. Updates export function useQueryParams( parseFn?: (query: string) => T, serializeFn?: (query: Partial) => string -): [T, (query: T, options?: setQueryParamsOptions) => void] - -export interface setQueryParamsOptions { - replace?: boolean - historyReplace?: boolean -} +): [T, (query: T, options?: { replace?: boolean, historyReplace?: boolean }) => void] ``` ## Basic