diff --git a/docs/api/useQueryParams.md b/docs/api/useQueryParams.md index 1fb73d0..c1e5364 100644 --- a/docs/api/useQueryParams.md +++ b/docs/api/useQueryParams.md @@ -12,10 +12,10 @@ 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?: { replace?: boolean, historyReplace?: boolean }) => void] ``` ## Basic @@ -44,7 +44,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 +59,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() + }) })