useSearchParamsShallow.ts 3.4 KB

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