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
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
| Name | Description | Type | Default |
|---|---|---|---|
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_ENGINE | InjectionToken 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 | — |
WrStorageEnvelope | The 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 } | — |