State

useReadLocalStorage

Read a localStorage (MDN, opens in a new tab) key without writing it, reactively.

Demo

useReadLocalStorage
null

The hook never writes — it observes hookli-docs-theme and re-renders on storage and local-storage events, including from other tabs.

Usage

use-read-local-storage.tsxtsx
import { useReadLocalStorage } from "hookli";

export function Demo() {
  const theme = useReadLocalStorage<string>("theme");

  return <p>Saved theme: {theme ?? "none"}</p>;
}

API

useReadLocalStorage.d.tsts
useReadLocalStorage<T>(key: string, options?: UseReadLocalStorageOptions<T>): T | null

Parameters

NameTypeDefaultDescription
keystringThe localStorage key to observe.
optionsUseReadLocalStorageOptions<T>{}Optional custom deserializer and hydration flag.

Returns

NameTypeDescription
valueT | nullThe parsed value, or null when the key is absent. Re-renders when the key changes in another tab (storage event) or via a local-storage event dispatched in this tab.

UseReadLocalStorageOptions<T>

Custom deserialization and hydration behaviour.

NameTypeDescription
deserializer(value: string) => TParse the stored string into a value. Defaults to JSON.parse, falling back to the raw string.
initializeWithValuebooleanRead localStorage synchronously on mount. Set false to defer to after hydration and avoid SSR mismatches.

Hook

The implementation, straight from hookli. Copy it, or install the package.

src/hooks/use-read-local-storage/use-read-local-storage.tstsx
import { useCallback, useEffect, useState } from "react";
import { useEventListener } from "../use-event-listener/use-event-listener";

interface UseReadLocalStorageOptions<T> {
  deserializer?: (value: string) => T;
  initializeWithValue?: boolean;
}

const IS_SERVER = typeof window === "undefined";

export function useReadLocalStorage<T>(
  key: string,
  options: UseReadLocalStorageOptions<T> = {},
): T | null {
  const { initializeWithValue = true } = options;

  const deserializer = useCallback(
    (value: string): T | undefined => {
      if (options.deserializer) return options.deserializer(value);
      if (value === "undefined") return undefined;
      try {
        return JSON.parse(value);
      } catch {
        return value as unknown as T;
      }
    },
    [options],
  );

  const readValue = useCallback((): T | null => {
    if (IS_SERVER) return null;
    try {
      const raw = window.localStorage.getItem(key);
      return raw ? (deserializer(raw) as T) : null;
    } catch {
      return null;
    }
  }, [key, deserializer]);

  const [storedValue, setStoredValue] = useState<T | null>(() =>
    initializeWithValue ? readValue() : null,
  );

  useEffect(() => {
    setStoredValue(readValue());
  }, [key]);

  const handleStorageChange = useCallback(
    (event: StorageEvent) => {
      if (event.key && event.key !== key) return;
      setStoredValue(readValue());
    },
    [key, readValue],
  );

  useEventListener("storage", handleStorageChange);
  useEventListener("local-storage", handleStorageChange);

  return storedValue;
}

View source on GitHub

Related hooks