State

useSessionStorage

useState backed by sessionStorage, synced across tabs.

Demo

Stored under hookli-docs-draft — survives a reload, cleared when the tab closes.

Usage

use-session-storage.tsxtsx
import { useSessionStorage } from "hookli";

export function Demo() {
  const [draft, setDraft, removeDraft] = useSessionStorage("draft", "");

  return (
    <div>
      <input value={draft} onChange={(e) => setDraft(e.target.value)} />
      <button type="button" onClick={removeDraft}>Clear</button>
    </div>
  );
}

API

useSessionStorage.d.tsts
useSessionStorage<T>(key: string, initialValue: T | (() => T), options?: UseSessionStorageOptions<T>): [T, (value: T | ((prev: T) => T)) => void, () => void]

Parameters

NameTypeDefaultDescription
keystringThe sessionStorage key to read and write.
initialValueT | (() => T)Value used before hydration and when the key is empty. Pass a function to compute it lazily.
optionsUseSessionStorageOptions<T>{}Optional custom serializer/deserializer and hydration flag.

Returns

NameTypeDescription
[0] valueTThe stored value. Server-rendered as initialValue, then hydrated from sessionStorage after mount.
[1] setValue(value: T | ((prev: T) => T)) => voidPersists to sessionStorage and updates state; accepts a value or an updater. Syncs every hook using the key in this tab.
[2] removeValue() => voidRemoves the key from sessionStorage and resets state to initialValue.

UseSessionStorageOptions<T>

Custom (de)serialization and hydration behaviour.

NameTypeDescription
serializer(value: T) => stringTurn the value into the string stored under the key. Defaults to JSON.stringify.
deserializer(value: string) => TParse the stored string back into a value. Defaults to JSON.parse.
initializeWithValuebooleanRead sessionStorage 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-session-storage/use-session-storage.tstsx
import { useCallback, useEffect, useState } from "react";
import { useEventCallback } from "../use-event-callback/use-event-callback";
import { useEventListener } from "../use-event-listener/use-event-listener";

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

type UseSessionStorageReturn<T> = [
  T,
  (value: T | ((prev: T) => T)) => void,
  () => void,
];

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

export function useSessionStorage<T>(
  key: string,
  initialValue: T | (() => T),
  options: UseSessionStorageOptions<T> = {},
): UseSessionStorageReturn<T> {
  const { initializeWithValue = true } = options;

  const serializer = useCallback(
    (value: T) => {
      if (options.serializer) return options.serializer(value);
      return JSON.stringify(value);
    },
    [options],
  );

  const deserializer = useCallback(
    (value: string): T => {
      if (options.deserializer) return options.deserializer(value);
      const defaultValue =
        initialValue instanceof Function ? initialValue() : initialValue;
      if (value === "undefined") return defaultValue;
      try {
        return JSON.parse(value);
      } catch {
        return defaultValue;
      }
    },
    [options, initialValue],
  );

  const readValue = useCallback((): T => {
    const initial =
      initialValue instanceof Function ? initialValue() : initialValue;
    if (IS_SERVER) return initial;
    try {
      const raw = window.sessionStorage.getItem(key);
      return raw ? deserializer(raw) : initial;
    } catch {
      return initial;
    }
  }, [initialValue, key, deserializer]);

  const [storedValue, setStoredValue] = useState<T>(() =>
    initializeWithValue
      ? readValue()
      : initialValue instanceof Function
        ? initialValue()
        : initialValue,
  );

  const setValue = useEventCallback((value: T | ((prev: T) => T)) => {
    if (IS_SERVER) return;
    try {
      const newValue = value instanceof Function ? value(readValue()) : value;
      window.sessionStorage.setItem(key, serializer(newValue));
      setStoredValue(newValue);
      window.dispatchEvent(new StorageEvent("session-storage", { key }));
    } catch {
      return;
    }
  });

  const removeValue = useEventCallback(() => {
    if (IS_SERVER) return;
    const defaultValue =
      initialValue instanceof Function ? initialValue() : initialValue;
    window.sessionStorage.removeItem(key);
    setStoredValue(defaultValue);
    window.dispatchEvent(new StorageEvent("session-storage", { key }));
  });

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

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

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

  return [storedValue, setValue, removeValue];
}

View source on GitHub

Related hooks