useReadLocalStorage


Custom hook that reads a value from localStorage, closely related to useLocalStorage().

Usage

import { useReadLocalStorage } from 'usehooks-ts'

export default function Component() {
  // Assuming a value was set in localStorage with this key
  const darkMode = useReadLocalStorage('darkMode')

  return <p>DarkMode is {darkMode ? 'enabled' : 'disabled'}</p>
}

API

useReadLocalStorage<T>(key, options): T | null | undefined

Custom hook that reads a value from localStorage, closely related to useLocalStorage().

Type parameters

NameDescription
TThe type of the stored value.

Parameters

NameTypeDescription
keystringThe key associated with the value in local storage.
optionsOptions<T, false>Additional options for reading the value (optional).

Returns

T | null | undefined

The stored value, or null if the key is not present or an error occurs.

useReadLocalStorage<T>(key, options?): T | null

Custom hook that reads a value from localStorage, closely related to useLocalStorage().

Type parameters

NameDescription
TThe type of the stored value.

Parameters

NameTypeDescription
keystringThe key associated with the value in local storage.
options?Partial<Options<T, true>>Additional options for reading the value (optional).

Returns

T | null

The stored value, or null if the key is not present or an error occurs.

Type aliases

Ƭ Options<T, InitializeWithValue>: Object

Represents the type for the options available when reading from local storage.

Type parameters

NameTypeDescription
TTThe type of the stored value.
InitializeWithValueextends boolean | undefined-

Type declaration

NameTypeDescription
deserializer?(value: string) => TCustom deserializer function to convert the stored string value to the desired type (optional).
initializeWithValueInitializeWithValueIf true (default), the hook will initialize reading the local storage. In SSR, you should set it to false, returning undefined initially.

Hook

import { useCallback, useEffect, useState } from 'react'

import { useEventListener } from 'usehooks-ts'

const IS_SERVER = typeof window === 'undefined'

type Options<T, InitializeWithValue extends boolean | undefined> = {
  deserializer?: (value: string) => T
  initializeWithValue: InitializeWithValue
}

// SSR version
export function useReadLocalStorage<T>(
  key: string,
  options: Options<T, false>,
): T | null | undefined
// CSR version
export function useReadLocalStorage<T>(
  key: string,
  options?: Partial<Options<T, true>>,
): T | null
export function useReadLocalStorage<T>(
  key: string,
  options: Partial<Options<T, boolean>> = {},
): T | null | undefined {
  let { initializeWithValue = true } = options
  if (IS_SERVER) {
    initializeWithValue = false
  }

  const deserializer = useCallback<(value: string) => T | null>(
    value => {
      if (options.deserializer) {
        return options.deserializer(value)
      }
      // Support 'undefined' as a value
      if (value === 'undefined') {
        return undefined as unknown as T
      }

      let parsed: unknown
      try {
        parsed = JSON.parse(value)
      } catch (error) {
        console.error('Error parsing JSON:', error)
        return null
      }

      return parsed as T
    },
    [options],
  )

  // Get from local storage then
  // parse stored json or return initialValue
  const readValue = useCallback((): T | null => {
    // Prevent build error "window is undefined" but keep keep working
    if (IS_SERVER) {
      return null
    }

    try {
      const raw = window.localStorage.getItem(key)
      return raw ? deserializer(raw) : null
    } catch (error) {
      console.warn(`Error reading localStorage key “${key}”:`, error)
      return null
    }
  }, [key, deserializer])

  const [storedValue, setStoredValue] = useState(() => {
    if (initializeWithValue) {
      return readValue()
    }
    return undefined
  })

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

  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 writeValueToLocalStorage
  // See: useLocalStorage()
  useEventListener('local-storage', handleStorageChange)

  return storedValue
}