useSessionStorage


Persist the state with session storage so that it remains after a page refresh. This can be useful to record session information. This hook is used in the same way as useState except that you must pass the storage key in the 1st parameter. If the window object is not present (as in SSR), useSessionStorage() will return the default value.

Related hooks:

Usage

import { useSessionStorage } from 'reactchemy'

export default function Component() {
   const [value, setValue] = useSessionStorage('test-key', 0)

   return (
    <div>
      <p>Count: {value}</p>
      <button onClick={() => setValue((x: number) => x + 1)}>Increment</button>
      <button onClick={() => setValue((x: number) => x - 1)}>Decrement</button>
    </div>
   )
}

Hook

import type {
   Dispatch,
   SetStateAction,
} from 'react'
import {
   useCallback,
   useEffect,
   useState,
} from 'react'

import { useEventCallback, useEventListener } from 'reactchemy'

declare global {
   interface WindowEventMap {
      'session-storage': CustomEvent
   }
}

type SetValue<T> = Dispatch<SetStateAction<T>>

export function useSessionStorage<T>(
   key: string,
   initialValue: T,
): [T, SetValue<T>] {
   // Get from session storage then
   // parse stored json or return initialValue
   const readValue = useCallback((): T => {
      // Prevent build error "window is undefined" but keep keep working
      if (typeof window === 'undefined')
         return initialValue

      try {
         const item = window.sessionStorage.getItem(key)
         return item ? (parseJSON(item) as T) : initialValue
      }
      catch (error) {
         console.warn(`Error reading sessionStorage key “${key}”:`, error)
         return initialValue
      }
   }, [initialValue, key])

   // State to store our value
   // Pass initial state function to useState so logic is only executed once
   const [storedValue, setStoredValue] = useState<T>(readValue)

   // Return a wrapped version of useState's setter function that ...
   // ... persists the new value to sessionStorage.
   const setValue: SetValue<T> = useEventCallback((value) => {
      // Prevent build error "window is undefined" but keeps working
      if (typeof window == 'undefined') {
         console.warn(
        `Tried setting sessionStorage key “${key}” even though environment is not a client`,
         )
      }

      try {
      // Allow value to be a function so we have the same API as useState
         const newValue = value instanceof Function ? value(storedValue) : value

         // Save to session storage
         window.sessionStorage.setItem(key, JSON.stringify(newValue))

         // Save state
         setStoredValue(newValue)

         // We dispatch a custom event so every useSessionStorage hook are notified
         window.dispatchEvent(new Event('session-storage'))
      }
      catch (error) {
         console.warn(`Error setting sessionStorage key “${key}”:`, error)
      }
   })

   useEffect(() => {
      setStoredValue(readValue())
      // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [])

   const handleStorageChange = useCallback(
      (event: StorageEvent | CustomEvent) => {
         if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key)
            return

         setStoredValue(readValue())
      },
      [key, readValue],
   )

   // this only works for other documents, not the current one
   useEventListener('storage', handleStorageChange)

   // this is a custom event, triggered in writeValueTosessionStorage
   // See: useSessionStorage()
   useEventListener('session-storage', handleStorageChange)

   return [storedValue, setValue]
}

// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
   try {
      return value === 'undefined' ? undefined : JSON.parse(value ?? '')
   }
   catch {
      console.log('parsing error on', { value })
      return undefined
   }
}