use-persisted-state.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  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. type UsePersistedStateOptions<Value, PersistedValue> = {
  11. listen?: boolean
  12. converter?: {
  13. toPersisted: (value: Value) => PersistedValue
  14. fromPersisted: (persisted: PersistedValue) => Value
  15. }
  16. }
  17. function usePersistedState<Value, PersistedValue = Value>(
  18. key: string,
  19. defaultValue?: Value,
  20. options?: UsePersistedStateOptions<Value, PersistedValue>
  21. ): [Value, Dispatch<SetStateAction<Value>>] {
  22. // Store the default value and options on first render so that they're stable
  23. // and use them on subsequent renders. This is important for, for example, a
  24. // non-primitive default value that should not change on every render.
  25. const [allOptions] = useState<{
  26. defaultValue?: Value
  27. options?: UsePersistedStateOptions<Value, PersistedValue>
  28. }>(() => ({ defaultValue, options }))
  29. const listen = allOptions.options?.listen || false
  30. const { toPersisted, fromPersisted } = allOptions.options?.converter || {}
  31. const storedDefaultValue = allOptions.defaultValue
  32. const getItem = useCallback(
  33. (key: string) => {
  34. const item = localStorage.getItem(key)
  35. return fromPersisted ? fromPersisted(item) : item
  36. },
  37. [fromPersisted]
  38. )
  39. const setItem = useCallback(
  40. (key: string, value: Value) => {
  41. // Nested ternary is convenient for type inference
  42. const val = toPersisted ? toPersisted(value) : value
  43. localStorage.setItem(key, val)
  44. },
  45. [toPersisted]
  46. )
  47. const [value, setValue] = useState<Value>(() => {
  48. return getItem(key) ?? storedDefaultValue
  49. })
  50. const updateFunction = useCallback(
  51. (newValue: SetStateAction<Value>) => {
  52. setValue(value => {
  53. const actualNewValue = _.isFunction(newValue)
  54. ? newValue(value)
  55. : newValue
  56. if (actualNewValue === storedDefaultValue) {
  57. localStorage.removeItem(key)
  58. } else {
  59. setItem(key, actualNewValue)
  60. }
  61. return actualNewValue
  62. })
  63. },
  64. [key, storedDefaultValue, setItem]
  65. )
  66. useEffect(() => {
  67. if (listen) {
  68. const listener = (event: StorageEvent) => {
  69. if (event.key === key) {
  70. // note: this value is read via getItem rather than from event.newValue
  71. // because getItem handles deserializing the JSON that's stored in localStorage.
  72. setValue(getItem(key) ?? storedDefaultValue)
  73. }
  74. }
  75. window.addEventListener('storage', listener)
  76. return () => {
  77. window.removeEventListener('storage', listener)
  78. }
  79. }
  80. }, [storedDefaultValue, key, listen, getItem])
  81. return [value, updateFunction]
  82. }
  83. export default usePersistedState