Hooks

useLocalStorage

A reactive, fully typed value backed by localStorage. It reads the stored value on mount, falling back to initial, writes through whenever current is assigned, and follows changes made in other tabs.

It is SSR safe: the server renders initial and the stored value arrives on hydration. Every storage access is wrapped, so private mode, a full quota or a hand-edited entry degrades instead of throwing.

Basic Usage

Read current reactively and assign to write through. Reload the page and the value is still here.

Persisted theme: system

Stored under sv5ui-docs-demo-theme.

<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  const theme = useLocalStorage('theme', 'system');
</script>

<p>Current theme: <strong>{theme.current}</strong></p>

<div class="flex gap-2">
  {#each ['system', 'light', 'dark'] as t}
    <button onclick={() => (theme.current = t)}>{t}</button>
  {/each}
</div>

Typed Objects

Pass a type parameter and an object as the initial value. Writing goes through the setter, so assign a new object rather than mutating a field in place.

fontSize 16px · compact false

<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  interface UserPrefs {
    fontSize: number;
    compact: boolean;
    language: string;
  }

  const prefs = useLocalStorage<UserPrefs>('user-prefs', {
    fontSize: 16,
    compact: false,
    language: 'en'
  });

  // Assign a whole object: the write goes through the setter, so mutating a
  // field in place would not reach storage.
  function toggleCompact() {
    prefs.current = { ...prefs.current, compact: !prefs.current.compact };
  }
</script>

<p>Font size: {prefs.current.fontSize}px</p>
<p>Compact: {prefs.current.compact}</p>
<button onclick={toggleCompact}>Toggle compact</button>

Keys That Change

The key can be a getter, and the getter is tracked. Point it at a route, a document id or the signed-in user, and the hook re-reads the new slot the moment it changes. Give each person below a different density and switch between them: the values follow the user, with no reload and no manual bookkeeping.

sv5ui-docs-demo-prefs:ana · comfortable
<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  let userId = $state('ana');

  // The key can be a getter, and the getter is tracked. When it returns a
  // different key the hook re-reads that slot, so two users on the same
  // device never see each other's settings and no manual reload is needed.
  const prefs = useLocalStorage(() => `prefs:${userId}`, { density: 'comfortable' });
</script>

<select bind:value={userId}>
  <option value="ana">Ana</option>
  <option value="minh">Minh</option>
</select>

<p>{prefs.current.density}</p>

Optional Persistence

A null or undefined key makes the hook inert: nothing is read, nothing is written, and no storage event is listened for. current keeps working as ordinary state, so a component with an optional storageKey prop has one code path rather than two. This is exactly what Resizable does.

enabled
not saved yet

Turn Persist off, write a value, reload the page: it is gone. Turn it on, write again, reload: it is still there.

<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  let { storageKey }: { storageKey?: string } = $props();

  // A null or undefined key makes the hook inert: nothing is read, nothing
  // is written, and no storage event is listened for. current behaves as
  // ordinary state. That is what lets a component offer opt-in persistence
  // without branching between two code paths.
  const layout = useLocalStorage(() => storageKey ?? null, [50, 50]);
</script>

<!-- enabled reports which mode the hook is in -->
{#if !layout.enabled}
  <p>This layout will not be remembered</p>
{/if}

<!-- Resizable does exactly this with its own storageKey prop -->

Removing a Value

remove() deletes the entry and returns current to initial without writing that value back. The distinction matters: assigning the default by hand would leave a copy of today's default in storage, and a later change to the default would never reach anyone who had pressed Reset.

{"fontSize":16,"compact":false}
<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  const prefs = useLocalStorage('user-prefs', defaults);
</script>

<!-- remove() deletes the entry and falls back to initial, without writing
     that fallback back into storage. The slot is genuinely empty afterwards,
     so a later default change is picked up rather than shadowed by a copy of
     the old one. -->
<button onclick={prefs.remove}>Reset to defaults</button>

Custom Serializer

JSON does not handle a Set, Map or Date, so hand over a pair of functions instead. A parse that throws is caught and the value falls back to initial, so a corrupted entry cannot take the page down.

[]
<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  // JSON does not handle Set, Map or Date, so hand over a serializer.
  const favorites = useLocalStorage('favorites', new Set<string>(), {
    serializer: {
      parse: (raw) => new Set<string>(JSON.parse(raw)),
      stringify: (value) => JSON.stringify([...value])
    }
  });

  function toggle(id: string) {
    const next = new Set(favorites.current);
    if (next.has(id)) next.delete(id);
    else next.add(id);
    favorites.current = next;
  }
</script>

<!-- A parse that throws is caught and the value falls back to initial, so a
     corrupted or hand-edited entry cannot break the page. -->

Cross-tab Sync

When another tab writes the same key, the browser fires a storage event and the hook follows it. Events are filtered by key and by storage area, so a session hook is never woken by a localStorage write and the other way round. The button below dispatches the event a second tab would produce.

theme system
<script lang="ts">
  import { useLocalStorage } from 'sv5ui';

  // Default: another tab writing the same key updates this one, through the
  // browser's storage event.
  const theme = useLocalStorage('theme', 'system');

  // Opt out when the value is per tab even though it is stored globally.
  const token = useLocalStorage('session-token', '', { syncTabs: false });
</script>

<!-- The storage event is filtered by area as well as by key, so a session
     hook is never woken by a localStorage write and the other way round. -->

Pass syncTabs: false when the value happens to live in shared storage but belongs to one tab only.

Session Storage

The storage option switches the backing store to sessionStorage, which lives for the life of the tab. useSessionStorage is the same call with the option already applied.

See useSessionStorage for when each store is the right one.

import { useLocalStorage, useSessionStorage } from 'sv5ui';

// Same hook, different backing store.
const draft = useLocalStorage('draft', '', { storage: 'session' });

// useSessionStorage is the same call with the option already set, and its
// options type has storage removed so it cannot be contradicted.
const sameThing = useSessionStorage('draft', '');

What Changed in v2.7.0

Existing calls are unaffected: every addition is opt-in.

AreaBefore
Key typestring only
Changing key at runtimeNot possible: one hook, one slot
Turning persistence offBranch between the hook and plain state by hand
Clearing an entryAssign the default back, which writes a copy of it
Backing storelocalStorage only
Cross-tab eventsFiltered by key

Parameters

ParameterTypeDefault
keyStorageKey | (() => StorageKey)-
initialT-

Options

OptionTypeDefault
serializer{ parse, stringify }JSON
syncTabsbooleantrue
storage'local' | 'session''local'

Return

PropertyType
currentT
enabledreadonly boolean
remove() => void