store2
store2 is a dependency-free wrapper around browser localStorage and sessionStorage. It serializes values through JSON, exposes explicit get, set, remove, clear, keys, iteration, merge, and read-modify-write helpers, and can partition keys into namespaces. It also includes a page-memory area that lasts until reload and accepts custom objects implementing the Web Storage interface. When browser storage cannot be opened, the core silently substitutes an in-memory implementation so calls keep working, although persistence is lost. TypeScript declarations are included and the default local area remains synchronous like the underlying Web Storage API.
A practical wrapper for small, non-sensitive browser preferences and legacy code that wants namespaced JSON storage. Do not mistake its friendly API or fake fallback for durable, atomic, secure, or high-capacity storage.
Use it if
- You want a small synchronous wrapper that stores and parses ordinary JSON values without repeating JSON.stringify and JSON.parse
- You need the same API for localStorage, sessionStorage, page-memory state, and namespaced key groups
- You maintain older browser code that already uses store2's callable shorthand, transact, add, or extension files
- You want no runtime dependencies and are comfortable treating unavailable storage as temporary in-memory state
- You store enough data for synchronous serialization or localStorage access to affect the main thread; use IndexedDB through idb-keyval or localForage for larger or frequent writes
- Persistence is mandatory: the README says unavailable localStorage or sessionStorage is silently replaced with fake page-memory storage, so writes can appear successful and vanish on reload unless you check isFake()
- You need atomic updates across tabs or workers: transact performs a separate get, callback, and set in the source, so two contexts can read the same old value and overwrite one another
- You need expiry, encryption, cross-tab subscriptions, quota recovery, or cookies in the supported core API; those capabilities live in separate extension files, several of which the README labels beta or alpha
- You plan to store authentication tokens, secrets, or sensitive user records: store2 adds JSON convenience, not isolation, and any successful same-origin script injection can read localStorage
- Your app is server-rendered and might call the imported singleton on the server: absent Web Storage causes the same fake-memory fallback, which can hide server/client state mistakes instead of throwing
Setup reality
npm install store2 is the entire package installation; there are no runtime dependencies and index.d.ts provides a default export. The difficult part is deciding where this synchronous browser singleton is safe to use. localStorage is shared by same-origin tabs and survives browser restarts, sessionStorage belongs to a tab session, and store.page is only an in-memory object for the current document. If localStorage access throws, including privacy or policy cases, store2 catches that during setup and swaps in fake memory. Check store.isFake() whenever persistence matters and show the user a real fallback or error. Values pass through JSON.stringify and JSON.parse: Date becomes a string unless you supply a reviver, Map and Set need a replacer, BigInt and circular structures throw, and class prototypes do not survive. Quotas vary by browser and origin; the core set path does not turn every quota failure into a durable alternative. Namespaces only prefix keys, so they do not provide access control. Calling clear() on the root local store invokes localStorage.clear() and removes every key for that origin, including keys written by other code; use a namespace if cleanup must be scoped. transact is synchronous convenience, not cross-tab locking. For SSR, import or call it only in client code, or explicitly provide a storage abstraction, because server use falls back to process memory. Expiry, storage events, overflow handling, and cookie areas require optional source extensions with their own maturity labels and are not part of the basic typed import.
Patterns
Store and retrieve a JSON valuestore-and-read-json
import store from 'store2';
store.set('preferences', { theme: 'dark', pageSize: 25 });
const preferences = store.get('preferences');
store.remove('preferences');Objects are copied through JSON serialization, so prototypes, undefined properties, and non-JSON types do not round-trip automatically.
Return a default when a key is absentprovide-default-value
const preferences = store.get('preferences', {
theme: 'system',
pageSize: 20,
});The alternate is returned only for a missing key. Malformed non-JSON storage is returned as its raw string rather than replaced by the default.
Keep state for one tab sessionuse-session-storage
store.session.set('checkout-step', 2);
const step = store.session.get('checkout-step', 1);
store.session.remove('checkout-step');sessionStorage is scoped to a browsing context and survives reloads in that tab, but it does not behave like shared localStorage across tabs.
Keep temporary state until reloaduse-page-memory
store.page.set('expanded-panels', ['filters', 'summary']);
const expanded = store.page.get('expanded-panels', []);store.page is store2's in-memory Storage implementation. It never persists across a page reload and is not shared with other tabs.
Isolate one feature's keysnamespace-feature-keys
const cartStore = store.namespace('cart');
cartStore.set('items', [{ sku: 'A1', quantity: 2 }]);
cartStore.set('currency', 'USD');
cartStore.clear();Namespacing prefixes physical keys such as cart.items. Clearing the namespace removes only matching keys, unlike root store.clear(), which clears the whole localStorage area.
Update a stored object with transactupdate-existing-value
store.transact('profile', (profile = { visits: 0 }) => ({
...profile,
visits: profile.visits + 1,
}));transact is a get followed by set and is not atomic across tabs, workers, or competing callbacks. Use a database transaction when lost updates matter.
Avoid overwriting an existing keyset-only-if-missing
const unusedValue = store.set(
'install-id',
crypto.randomUUID(),
false,
);The return contract is unusual: set normally returns the previous value, but with overwrite false and an existing key it returns the unused new value. Call has() first when clarity matters.
Set and retrieve multiple keysread-write-many
store.setAll({
locale: 'en-GB',
compactMode: true,
});
const allLocalValues = store.getAll();getAll reads every key in the current area or namespace. On the root store that includes data written by unrelated code on the same origin.
Append arrays or merge plain objectsmerge-or-append
store.set('recent-searches', ['printer']);
store.add('recent-searches', ['scanner']);
store.set('flags', { beta: false });
store.add('flags', { compact: true });add concatenates arrays, shallow-merges objects of the same type, and uses + for other existing values. It is convenient but can coerce mixed primitive types unexpectedly.
Inspect keys without exposing raw JSON stringsiterate-keys
for (const key of store.keys()) {
console.log(key, store.get(key));
}
store.each((key, value) => {
if (value == null) store.remove(key);
});The source adjusts iteration when callbacks remove entries, but mutation while iterating browser storage is still harder to reason about than collecting keys first.
Restore Date values with a reviverround-trip-dates
store.set('job', { finishedAt: new Date().toISOString() });
const job = store.get('job', (key, value) => {
return key === 'finishedAt' ? new Date(value) : value;
});The second get argument is treated as a JSON.parse reviver when it is a function. Match known keys or tagged values so ordinary strings are not converted accidentally.
Refuse silent memory fallback when persistence mattersdetect-fake-storage
if (store.isFake()) {
throw new Error('Persistent browser storage is unavailable');
}
store.set('draft', draft);store2 swaps to fake memory when its startup storage test throws. Without this check, writes appear to work but disappear when the document closes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| idb-keyval | npm | Choose it for a tiny Promise-based IndexedDB wrapper that handles larger values without synchronous localStorage calls |
| localforage | npm | Choose it for an asynchronous storage API with IndexedDB-first drivers and broader fallback behavior |
| localstorage-slim | npm | Choose it when built-in TTL and optional value obfuscation matter more than store2's namespaces and callable API |
| unstorage | npm | Choose it for an async storage abstraction with swappable browser, server, memory, filesystem, and remote drivers |