useUrlState.ts65 lines · main
1import { useRouter } from 'next/router'
2import { useCallback, useMemo, type Dispatch, type SetStateAction } from 'react'
3
4import useLatest from '@/hooks/misc/useLatest'
5
6export type UrlStateParams = {
7 [k: string]: string | string[] | undefined
8}
9
10/** @deprecated Use useQueryState from nuqs instead for URL state */
11export function useUrlState<ValueParams extends UrlStateParams>({
12 replace = true,
13 arrayKeys = [],
14}: {
15 /** Whether to use push state routing (working back button), or just replace the current URL
16 * @default true
17 */
18 replace?: boolean
19 arrayKeys?: string[]
20} = {}): [ValueParams, Dispatch<SetStateAction<ValueParams>>] {
21 const stringifiedArrayKeys = JSON.stringify(arrayKeys)
22 // eslint-disable-next-line react-hooks/exhaustive-deps
23 const arrayKeysSet = useMemo(() => new Set(arrayKeys), [stringifiedArrayKeys])
24 const router = useRouter()
25
26 const params: ValueParams = useMemo(() => {
27 return Object.fromEntries(
28 Object.entries(router.query).map(([key, value]) => {
29 if (arrayKeysSet.has(key)) {
30 return Array.isArray(value) ? [key, value] : [key, [value]]
31 }
32
33 return [key, value]
34 })
35 )
36 }, [arrayKeysSet, router.query])
37
38 const paramsRef = useLatest(params)
39
40 const setParams: Dispatch<SetStateAction<ValueParams>> = useCallback(
41 (newParams) => {
42 const params = paramsRef.current
43
44 const nextParams = typeof newParams === 'function' ? newParams(params) : newParams
45 let newQuery = Object.fromEntries(
46 Object.entries({ ...params, ...nextParams }).filter(([, value]) => Boolean(value))
47 )
48
49 const replaceOrPush = replace ? router.replace : router.push
50
51 replaceOrPush(
52 {
53 pathname: router.pathname,
54 query: newQuery,
55 },
56 undefined,
57 { shallow: true, scroll: false }
58 )
59 },
60 // eslint-disable-next-line react-hooks/exhaustive-deps
61 [router, replace]
62 )
63
64 return [params, setParams] as const
65}