use-persisted-state.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import {
  2. useState,
  3. useCallback,
  4. useEffect,
  5. SetStateAction,
  6. Dispatch,
  7. } from 'react'
  8. import _ from 'lodash'
  9. import localStorage from '../../infrastructure/local-storage'
  10. import { debugConsole } from '@/utils/debugging'
  11. const safeStringify = (value: unknown) => {
  12. try {
  13. return JSON.stringify(value)
  14. } catch (e) {
  15. debugConsole.error('double stringify exception', e)
  16. return null
  17. }
  18. }
  19. const safeParse = (value: string) => {
  20. try {
  21. return JSON.parse(value)
  22. } catch (e) {
  23. debugConsole.error('double parse exception', e)
  24. return null
  25. }
  26. }
  27. function usePersistedState<T = any>(
  28. key: string,
  29. defaultValue?: T,
  30. listen = false,
  31. // The option below is for backward compatibility with Angular
  32. // which sometimes stringifies the values twice
  33. doubleStringifyAndParse = false
  34. ): [T, Dispatch<SetStateAction<T>>] {
  35. const getItem = useCallback(
  36. (key: string) => {
  37. const item = localStorage.getItem(key)
  38. return doubleStringifyAndParse ? safeParse(item) : item
  39. },
  40. [doubleStringifyAndParse]
  41. )
  42. const setItem = useCallback(
  43. (key: string, value: unknown) => {
  44. const val = doubleStringifyAndParse ? safeStringify(value) : value
  45. localStorage.setItem(key, val)
  46. },
  47. [doubleStringifyAndParse]
  48. )
  49. const [value, setValue] = useState<T>(() => {
  50. return getItem(key) ?? defaultValue
  51. })
  52. const updateFunction = useCallback(
  53. (newValue: SetStateAction<T>) => {
  54. setValue(value => {
  55. const actualNewValue = _.isFunction(newValue)
  56. ? newValue(value)
  57. : newValue
  58. if (actualNewValue === defaultValue) {
  59. localStorage.removeItem(key)
  60. } else {
  61. setItem(key, actualNewValue)
  62. }
  63. return actualNewValue
  64. })
  65. },
  66. [key, defaultValue, setItem]
  67. )
  68. useEffect(() => {
  69. if (listen) {
  70. const listener = (event: StorageEvent) => {
  71. if (event.key === key) {
  72. // note: this value is read via getItem rather than from event.newValue
  73. // because getItem handles deserializing the JSON that's stored in localStorage.
  74. setValue(getItem(key) ?? defaultValue)
  75. }
  76. }
  77. window.addEventListener('storage', listener)
  78. return () => {
  79. window.removeEventListener('storage', listener)
  80. }
  81. }
  82. }, [defaultValue, key, listen, getItem])
  83. return [value, updateFunction]
  84. }
  85. export default usePersistedState