Skip to content

useLocalStorage ​

Custom hook that uses the localStorage API to persist state across page reloads.

Usage ​

tsx
import { useLocalStorage } from 'vhooks'

export default function Component() {
  const [value, setValue, removeValue] = useLocalStorage('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>
      <button
        onClick={() => {
          removeValue()
        }}
      >
        Reset
      </button>
    </div>
  )
}

Installation ​

sh
pnpm dlx scaflo@latest https://raw.githubusercontent.com/programming-with-ia/vDocs/scaflos/hooks/useLocalStorage.json -e %src%/hooks
sh
npx scaflo@latest https://raw.githubusercontent.com/programming-with-ia/vDocs/scaflos/hooks/useLocalStorage.json -e %src%/hooks
sh
bunx scaflo@latest https://raw.githubusercontent.com/programming-with-ia/vDocs/scaflos/hooks/useLocalStorage.json -e %src%/hooks
sh
yarn dlx scaflo@latest https://raw.githubusercontent.com/programming-with-ia/vDocs/scaflos/hooks/useLocalStorage.json -e %src%/hooks

API ​

▸ useLocalStorage<T>(key, initialValue, options?): [T, Dispatch<SetStateAction<T>>, () => void]

Custom hook that uses the localStorage API to persist state across page reloads.

Type parameters ​

NameDescription
TThe type of the state to be stored in local storage.

Parameters ​

NameTypeDescription
keystringThe key under which the value will be stored in local storage.
initialValueT | () => TThe initial value of the state or a function that returns the initial value.
options?UseLocalStorageOptions<T>Options for customizing the behavior of serialization and deserialization (optional).

Returns ​

[T, Dispatch<SetStateAction<T>>, () => void]

A tuple containing the stored value, a function to set the value and a function to remove the key from storage.

Type aliases ​

Ƭ UseLocalStorageOptions<T>: Object

Options for customizing the behavior of serialization and deserialization.

Type parameters ​

NameDescription
TThe type of the state to be stored in local storage.

Type declaration ​

NameTypeDescription
deserializer?(value: string) => T-
initializeWithValue?booleanIf true (default), the hook will initialize reading the local storage. In SSR, you should set it to false, returning the initial value initially. Default ts true
serializer?(value: T) => string-

Hook ​

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

import type { Dispatch, SetStateAction } from 'react'

import { useEventCallback, useEventListener } from 'vhooks'

declare global {
  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
  interface WindowEventMap {
    'local-storage': CustomEvent
  }
}

type UseLocalStorageOptions<T> = {
    serializer?: (value: T) => string
    deserializer?: (value: string) => T
    initializeWithValue?: boolean
}

const IS_SERVER = typeof window === 'undefined'

export function useLocalStorage<T>(
  key: string,
  initialValue: T | (() => T),
  options: UseLocalStorageOptions<T> = {},
): [T, Dispatch<SetStateAction<T>>, () => void] {
  const { initializeWithValue = true } = options

  const serializer = useCallback<(value: T) => string>(
    value => {
      if (options.serializer) {
        return options.serializer(value)
      }

      return JSON.stringify(value)
    },
    [options],
  )

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

      const defaultValue =
        initialValue instanceof Function ? initialValue() : initialValue

      let parsed: unknown
      try {
        parsed = JSON.parse(value)
      } catch (error) {
        console.error('Error parsing JSON:', error)
        return defaultValue // Return initialValue if parsing fails
      }

      return parsed as T
    },
    [options, initialValue],
  )

  // Get from local storage then
  // parse stored json or return initialValue
  const readValue = useCallback((): T => {
    const initialValueToUse =
      initialValue instanceof Function ? initialValue() : initialValue

    // Prevent build error "window is undefined" but keep working
    if (IS_SERVER) {
      return initialValueToUse
    }

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

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

    return initialValue instanceof Function ? initialValue() : initialValue
  })

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue: Dispatch<SetStateAction<T>> = useEventCallback(value => {
    // Prevent build error "window is undefined" but keeps working
    if (IS_SERVER) {
      console.warn(
        `Tried setting localStorage 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(readValue()) : value

      // Save to local storage
      window.localStorage.setItem(key, serializer(newValue))

      // Save state
      setStoredValue(newValue)

      // We dispatch a custom event so every similar useLocalStorage hook is notified
      window.dispatchEvent(new StorageEvent('local-storage', { key }))
    } catch (error) {
      console.warn(`Error setting localStorage key “${key}”:`, error)
    }
  })

  const removeValue = useEventCallback(() => {
    // Prevent build error "window is undefined" but keeps working
    if (IS_SERVER) {
      console.warn(
        `Tried removing localStorage key “${key}” even though environment is not a client`,
      )
    }

    const defaultValue =
      initialValue instanceof Function ? initialValue() : initialValue

    // Remove the key from local storage
    window.localStorage.removeItem(key)

    // Save state with default value
    setStoredValue(defaultValue)

    // We dispatch a custom event so every similar useLocalStorage hook is notified
    window.dispatchEvent(new StorageEvent('local-storage', { key }))
  })

  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, setValue, removeValue]
}

vDocs