Service

WrStorage

Reactive key/value storage on top of any Storage-compatible engine. Swap the engine through DI (defaults to localStorage with an in-memory SSR / private-mode fallback). Built-in prefixing, TTL, and signal-based watch() for local + cross-tab updates.

Install

import { provideWrStorage, WrStorage } from 'ngwr/storage';

bootstrapApplication(AppComponent, {
  providers: [
    // Optional — defaults to localStorage with an in-memory SSR fallback.
    provideWrStorage({ prefix: 'myapp:', ttl: 24 * 60 * 60 * 1000 }),
  ],
});

Swap the engine

// Swap the engine globally (e.g. sessionStorage for a tab-only app):
provideWrStorage({ engine: sessionStorage })

// Or lazily — useful when wrapping with encryption / IndexedDB / a worker bridge:
provideWrStorage({ engine: () => new EncryptedStorage(localStorage, key) })

// Or directly via the token (per-feature overrides through nested injectors):
providers: [{ provide: WR_STORAGE_ENGINE, useValue: sessionStorage }]

Usage

private readonly store = inject(WrStorage);

this.store.set('user', { name: 'Ada' });
this.store.get<{ name: string }>('user');         // → { name: 'Ada' }

this.store.set('cart', items, { ttl: 60_000 });   // expires in 60s
this.store.has('cart');                            // true → false after 60s

const theme = this.store.watch<'light' | 'dark'>('theme', 'light');
effect(() => console.log('theme is', theme()));

What lands in storage

set() writes an envelope, not the bare value: a JSON object with the value under v, plus an e expiry in epoch milliseconds when a TTL applies. That is a wire format rather than an implementation detail, because anything reading the same key from outside Angular has to unwrap it — a pre-paint theme script in index.html, a service worker, a native shell, a second framework on the same origin.

import type { WrStorageEnvelope } from 'ngwr/storage';

// provideWrStorage({ prefix: 'myapp:' })
store.set('theme', 'dark');
// localStorage['myapp:theme'] === '{"v":"dark"}'

store.set('cart', [{ sku: 'a1' }], { ttl: 60_000 });
// localStorage['myapp:cart'] === '{"v":[{"sku":"a1"}],"e":1767225600000}'

// Reading the same key from outside Angular — a pre-paint script, a worker:
const raw = localStorage.getItem('myapp:theme');
const env = raw === null ? null : (JSON.parse(raw) as WrStorageEnvelope<string>);
const value = env && (env.e === undefined || env.e >= Date.now()) ? env.v : null;

Three rules follow from it. A key holding anything else — a bare JSON value, or a string that is not JSON at all — is returned as it stands, so keys written by non-ngwr code keep working. A value past its e reads as absent and is removed on the next get(). And prefix is applied to the key on the way in, so the raw key is prefix + key — an outside reader has to prepend it too.

Turning the envelope off with json: false stores String(value) verbatim, which is the right choice when a key is shared with code you do not control — and gives up TTL and non-string values with it.

Live demo

Reactive read via store.watch('demo:visits'): 0

TTL (10s) — stash a note then refresh: — empty —

Open this page in two tabs and click Bump visits in one — the other updates via the storage event.

Why ngwr provides this

Direct localStorage access throws in SSR and private-mode Safari, and stringly-typed keys drift. This wraps web storage with JSON (de)serialization, SSR no-ops, and typed reads.

API

NameDescriptionTypeDefault
get(key, fallback?)Read a value. Returns fallback (default null) when absent or expired.<T>(k: string, f?: T) => T | null
set(key, value, opts?)Write a value. Per-call ttl (ms) overrides config default.<T>(k: string, v: T, opts?: { ttl?: number }) => void
remove(key)Remove the value at key.(k: string) => void
clear()Clear all keys under our prefix (or everything when prefix is empty).() => void
has(key)Whether the key is present (ignoring expiry).(k: string) => boolean
keys()All known keys with the prefix stripped.() => readonly string[]
watch(key, fallback?)Reactive read. Updates on local writes and cross-tab storage events.<T>(k: string, f?: T) => Signal<T | null>
provideWrStorage(opts?)Configure prefix, json, ttl, and swap the engine.(opts?) => EnvironmentProviders
WR_STORAGE_ENGINEInjectionToken for the active Storage engine — override at any level to swap.InjectionToken<Storage>localStorage / memory fallback
createMemoryStorage()Map-backed Storage shim. Useful for tests.() => Storage
WrStorageEnvelopeThe on-disk format written while json is on — the value under v, plus an epoch-ms expiry e when a TTL applies.{ readonly v: T; readonly e?: number }

See also