useSearchParamsShallow.ts106 lines · main
1'use client'
2
3import { useCallback, useEffect, useId, useMemo, useReducer, useRef } from 'react'
4
5/**
6 * Stores state in search params while bypassing Next Router.
7 *
8 * The purpose of this is to use search params while maintaining SSG ability,
9 * because Next.js's `useSearchParams` forces at least the children to be
10 * client-side rendered on static routes.
11 *
12 * See https://nextjs.org/docs/app/api-reference/functions/use-search-params
13 */
14const useSearchParamsShallow = () => {
15 const EVENT_NAME = 'briven.events.packages.common.useSearchParamsShallow'
16 const id = useId()
17 const timeoutHandle = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
18
19 const reducer = useCallback(
20 (_: URLSearchParams, action: { target: 'int' | 'ext'; newParams: URLSearchParams }) => {
21 clearTimeout(timeoutHandle.current)
22 if (action.target === 'ext') {
23 /**
24 * Doing this in the next tick makes sure that the originating
25 * component finishes rendering before it triggers updates to other
26 * components.
27 */
28 timeoutHandle.current = setTimeout(() => {
29 document.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: { id } }))
30 })
31 }
32 return action.newParams
33 },
34 [id]
35 )
36
37 useEffect(() => () => clearTimeout(timeoutHandle.current), [])
38
39 const [localParams, setLocalParams] = useReducer(reducer, undefined, () => new URLSearchParams())
40
41 useEffect(() => {
42 const handler = (event: CustomEvent<{ id: string }>) => {
43 if (event.detail.id !== id) {
44 const globalParams = new URLSearchParams(window.location.search)
45 setLocalParams({ target: 'int', newParams: globalParams })
46 }
47 }
48
49 document.addEventListener(EVENT_NAME, handler as EventListener)
50 return () => document.removeEventListener(EVENT_NAME, handler as EventListener)
51 }, [id])
52
53 useEffect(() => {
54 const globalParams = new URLSearchParams(window.location.search)
55 setLocalParams({ target: 'int', newParams: globalParams })
56 }, [])
57
58 const has = useCallback((key: string) => localParams.has(key), [localParams])
59
60 const get = useCallback((key: string) => localParams.get(key), [localParams])
61
62 const getAll = useCallback((key: string) => localParams.getAll(key), [localParams])
63
64 const set = useCallback((key: string, value: any) => {
65 if (typeof window === 'undefined') return
66
67 const url = new URL(window.location.href)
68 url.searchParams.set(key, value)
69 window.history.replaceState(null, '', url)
70 setLocalParams({ target: 'ext', newParams: url.searchParams })
71 }, [])
72
73 const append = useCallback((key: string, value: any) => {
74 if (typeof window === 'undefined') return
75
76 const url = new URL(window.location.href)
77 url.searchParams.append(key, value)
78 window.history.replaceState(null, '', url)
79 setLocalParams({ target: 'ext', newParams: url.searchParams })
80 }, [])
81
82 const _delete = useCallback((key: string) => {
83 if (typeof window === 'undefined') return
84
85 const url = new URL(window.location.href)
86 url.searchParams.delete(key)
87 window.history.replaceState(null, '', url)
88 setLocalParams({ target: 'ext', newParams: url.searchParams })
89 }, [])
90
91 const api = useMemo(
92 () => ({
93 has,
94 get,
95 getAll,
96 append,
97 set,
98 delete: _delete,
99 }),
100 [has, get, getAll, append, set, _delete]
101 )
102
103 return api
104}
105
106export { useSearchParamsShallow }