Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 24 additions & 5 deletions docs/api/useQueryParams.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends QueryParam>(
parseFn?: (query: string) => T,
serializeFn?: (query: Partial<T>) => string
): [T, (query: T, options?: { replace?: boolean, historyReplace?: boolean }) => void]
```

## Basic
Expand Down Expand Up @@ -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'
Expand All @@ -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 (
<input value={startsWith || ''} onChange={handleChange} />
)
}
```

> 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
Expand Down
5 changes: 3 additions & 2 deletions src/querystring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface QueryParam {

export interface setQueryParamsOptions {
replace?: boolean
historyReplace?: boolean
}

export function useQueryParams<T extends QueryParam>(
Expand All @@ -19,15 +20,15 @@ export function useQueryParams<T extends QueryParam>(
): [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()

if (serialized) path += '?' + serialized
if (!replace) path += getCurrentHash()

navigate(path)
navigate(path, { replace: historyReplace })
},
[querystring, parseFn, serializeFn]
)
Expand Down
34 changes: 32 additions & 2 deletions test/querystring.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<button data-testid="update" onClick={() => setQuery({ foo }, { replace })}>
<button data-testid="update" onClick={() => setQuery({ foo }, { replace, historyReplace })}>
Set Query: {query.foo}
</button>
)
Expand Down Expand Up @@ -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(<Route historyReplace={true} />)
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(<Route historyReplace={false} />)
act(() => void fireEvent.click(getByTestId('update')))
expect(pushStateSpy).toHaveBeenCalled()
expect(document.location.search).toEqual('?foo=bar')
pushStateSpy.mockRestore()
})
})
Loading