useLocalStorage.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. // Reference: https://usehooks.com/useLocalStorage/
  2. import { useQuery, useQueryClient } from '@tanstack/react-query'
  3. import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'
  4. export function useLocalStorage<T>(key: string, initialValue: T) {
  5. // State to store our value
  6. // Pass initial state function to useState so logic is only executed once
  7. const [storedValue, setStoredValue] = useState<T>(() => {
  8. if (typeof window === 'undefined') {
  9. return initialValue
  10. }
  11. try {
  12. // Get from local storage by key
  13. const item = window.localStorage.getItem(key)
  14. // Parse stored json or if none return initialValue
  15. return item ? JSON.parse(item) : initialValue
  16. } catch (error) {
  17. // If error also return initialValue
  18. console.log(error)
  19. return initialValue
  20. }
  21. })
  22. // Return a wrapped version of useState's setter function that ...
  23. // ... persists the new value to localStorage.
  24. const setValue = useCallback(
  25. (value: T | ((val: T) => T)) => {
  26. try {
  27. // Allow value to be a function so we have same API as useState
  28. const valueToStore = value instanceof Function ? value(storedValue) : value
  29. // Save state
  30. setStoredValue(valueToStore)
  31. // Save to local storage
  32. if (typeof window !== 'undefined') {
  33. window.localStorage.setItem(key, JSON.stringify(valueToStore))
  34. }
  35. } catch (error) {
  36. // A more advanced implementation would handle the error case
  37. console.log(error)
  38. }
  39. },
  40. [key, storedValue]
  41. )
  42. return [storedValue, setValue] as const
  43. }
  44. /**
  45. * Hook to load/store values from local storage with an API similar
  46. * to `useState()`.
  47. *
  48. * Differs from `useLocalStorage()` in that it uses `react-query` to
  49. * invalidate stale values across hooks with the same key.
  50. */
  51. export function useLocalStorageQuery<T>(key: string, initialValue: T) {
  52. const queryClient = useQueryClient()
  53. const queryKey = useMemo(() => ['localStorage', key], [key])
  54. const {
  55. error,
  56. data: storedValue = initialValue,
  57. isSuccess,
  58. isLoading,
  59. isError,
  60. } = useQuery({
  61. queryKey,
  62. queryFn: () => {
  63. if (typeof window === 'undefined') {
  64. return initialValue
  65. }
  66. const item = window.localStorage.getItem(key)
  67. if (!item) {
  68. return initialValue
  69. }
  70. return JSON.parse(item) as T
  71. },
  72. })
  73. const setValue: Dispatch<SetStateAction<T>> = useCallback(
  74. (value) => {
  75. const currentValue = queryClient.getQueryData<T>(queryKey) ?? initialValue
  76. const valueToStore = value instanceof Function ? value(currentValue) : value
  77. // Bail out when the value is unchanged (matches useState semantics).
  78. // Without this, no-op updates from consumers — like a pruning effect
  79. // whose updater returns `current` unchanged — still write to
  80. // localStorage and invalidate the query, which churns subscribers and
  81. // can cascade into "Maximum update depth exceeded" when two consumers
  82. // of the same key are mounted together.
  83. if (Object.is(valueToStore, currentValue)) return
  84. if (typeof window !== 'undefined') {
  85. window.localStorage.setItem(key, JSON.stringify(valueToStore))
  86. }
  87. queryClient.setQueryData(queryKey, valueToStore)
  88. queryClient.invalidateQueries({ queryKey })
  89. },
  90. // initialValue is intentionally excluded: the function body reads the
  91. // current value via queryClient.getQueryData so it doesn't close over
  92. // initialValue reactively — including it would cause a new function
  93. // reference every render when callers pass an inline literal (e.g. []).
  94. // eslint-disable-next-line react-hooks/exhaustive-deps
  95. [key, queryKey, queryClient]
  96. )
  97. return [storedValue, setValue, { isSuccess, isLoading, isError, error }] as const
  98. }