ultrastorage README

The project's README from github.com/yangshun/ultrastorage. As one markdown file: README.md.

ultrastorage

Adds rich-value serialization, expiration, namespacing, and schema validation to localStorage.

Get started at ultrastorage.dev for setup instructions, guides, and examples.

Documentation · API reference · React guide

Features

Installation

npm install ultrastorage

ultrastorage was previously published as greatstorage. Existing users can follow the migration guide.

How it works

Internally, every value is stored as an object rather than being written to the storage raw. That object carries the user value, an optional expiry, a version, and an internal __us marker. The marker is there so ultrastorage can reliably tell its own entries apart from unrelated keys already living in the same Storage. The version is there so the on-disk format can evolve later without guessing which shape an older entry used if this library ever needs to change the internal storage structure.

New writes use __us; existing entries marked with __gs remain fully supported without migration.

Refer to the documentation for more explanation regarding internals and design decisions.

Usage

The examples below cover the basics. Follow the getting started guide for a walkthrough, or browse the documentation for detailed guides.

Basic

Store and retrieve objects without calling JSON.stringify() and JSON.parse().

// lib/app-storage.ts
import { createStorage } from 'ultrastorage';

// Create an app-wide singleton instance.
export const appStorage = createStorage({ prefix: 'acme' });
// app.ts
import { appStorage } from './lib/app-storage';

appStorage.setItem('user', { name: 'Alice', age: 30 });
appStorage.getItem('user'); // { name: 'Alice', age: 30 }

Array keys

Use an array of strings when a key has several structural parts. Arrays with the same ordered segments address the same entry, even when they are different array instances.

appStorage.setItem(['users', userId, 'preferences'], { theme: 'dark' });
appStorage.getItem(['users', userId, 'preferences']); // { theme: 'dark' }

Array keys use an opaque internal encoding rather than the configured namespace separator, so separator characters and empty strings inside segments are preserved.

Store anything

Store Set, Map, Date, and other values that JSON does not preserve.

Uses devalue by default, but you can bring your own serializer.

appStorage.setItem('tags', new Set(['a', 'b', 'c']));
appStorage.getItem('tags'); // Set {'a', 'b', 'c'}

appStorage.setItem('metadata', new Map([['key', 'value']]));
appStorage.getItem('metadata'); // Map {'key' => 'value'}

appStorage.setItem('date', new Date('2025-01-01'));
appStorage.getItem('date'); // Date 2025-01-01T00:00:00.000Z

TTL / expiration

Set a lifetime or an absolute expiration time for stored data.

Expired data behaves like a missing key. getItem() removes expired entries on read, while has(), key(), keys(), and length simply ignore them. Use clearExpired() to proactively sweep them.

// Expires in 60 seconds
appStorage.setItem('token', 'abc123', { ttl: 60_000 });

// Expires at a specific date
appStorage.setItem('session', { id: 1 }, { expiresAt: new Date('2025-12-31') });

// Expired items return null
appStorage.getItem('token'); // null (after 60s)

Namespacing

Add a prefix to group related keys and avoid collisions with other storage users.

const appStorage = createStorage({ prefix: 'acme' });

appStorage.setItem('theme', 'dark'); // stored as "acme:theme"
appStorage.getItem('theme'); // 'dark'

localStorage.getItem('theme'); // null

// clear() only removes keys within the namespace
appStorage.clear();

Subscribe to changes

Subscribe to a key to update other parts of the page when its storage entry changes. Other ultrastorage instances using the same Storage object and fully prefixed key notify the same listeners, even if those instances never subscribe themselves.

const appStorage = createStorage({ prefix: 'app' });

function applyTheme() {
  document.documentElement.dataset.theme = appStorage.getItem<string>('theme') ?? 'system';
}

const unsubscribe = appStorage.subscribe('theme', (change) => {
  console.log(change.key, change.type, change.source);
  // 'theme', 'set' | 'remove' | 'expire', 'local' | 'external'
  applyTheme();
});

applyTheme(); // Subscribing does not invoke the listener immediately.

Another module can create its own instance with the same prefix and notify the subscriber above:

import { createStorage } from 'ultrastorage';

const appStorage = createStorage({ prefix: 'app' });
appStorage.setItem('theme', 'dark'); // Notifies this page synchronously.

When the subscriber is no longer needed, clean it up in the subscribing module:

unsubscribe(); // Safe to call again; each subscription is independent.

Tabs sharing localStorage receive changes asynchronously through browser storage events. sessionStorage remains scoped to its tab; in-memory and custom Storage objects support same-page notifications between instances using the exact same object.

Events describe storage changes, without carrying old/new values. Read the current value using getItem(), optionally with a schema. It may reflect a later write by the time you read it. Expiration stays lazy by default. Pass { reactiveExpiration: true } as the third argument to subscribe() to receive expire when the deadline passes, without deleting the entry. React hooks accept the same option. Later cleanup through getItem() or clearExpired() may emit another expire event. Timers may run late.

Check, remove, and clear

Check for entries, remove individual entries, or clear a namespace.

appStorage.has('user'); // true
appStorage.removeItem('user');
appStorage.has('user'); // false

appStorage.clear(); // remove all entries written by `ultrastorage`
appStorage.clearExpired(); // remove only expired entries

Type-safe access

Provide a type argument to type values returned from storage.

interface User {
  name: string;
  age: number;
}

const user = appStorage.getItem<User>('user');
// user is typed as User | null

appStorage.getOrInit<User>('user', () => ({ name: 'Alice', age: 30 }));

appStorage.updateItem<User>('user', (current) => ({
  ...current!,
  age: current!.age + 1,
}));

However, the true safe way is to validate with a schema during read.

Schema validation

Browser storage can be modified outside your application. Validate reads with any library that supports synchronous Standard Schema validation.

import { z } from 'zod';

const UserSchema = z.object({ name: z.string(), age: z.number() });

// Returns typed value if valid, null if validation fails
const user = appStorage.getItem('user', { schema: UserSchema });

Not just localStorage

Pass localStorage, sessionStorage, or any object that implements Storage. Use createMemoryStorage() for tests and server-side rendering.

import { createStorage, createMemoryStorage } from 'ultrastorage';

const appStorage = createStorage({
  prefix: 'acme',
  storage: typeof window === 'undefined' ? createMemoryStorage() : undefined,
});

Custom serializer

Provide custom stringify and parse methods to use another serialization format. Reads use only that parser by default. To try a second synchronous parser after it throws, set fallbackParser, for example fallbackParser: JSON.parse for older JSON envelopes. See fallback parsing and migration.

import superjson from 'superjson';

const appStorage = createStorage({
  prefix: 'acme',
  serializer: { stringify: superjson.stringify, parse: superjson.parse },
});

If you provide your own serializer and want to keep devalue out of your bundle entirely, import from ultrastorage/core instead. The only difference is that serializer is required.

import { createStorage } from 'ultrastorage/core';
import superjson from 'superjson';

const appStorage = createStorage({
  prefix: 'acme',
  serializer: { stringify: superjson.stringify, parse: superjson.parse },
});

Additional APIs

Use these helpers for common initialization and update operations.

appStorage.getOrInit()

Return the existing value, or create, store, and return a new value when the key is missing.

const prefs = appStorage.getOrInit('prefs', () => ({
  theme: 'light',
  lang: 'en',
}));

appStorage.updateItem()

Read, update, and write a value in one call.

appStorage.updateItem('count', (current) => (current ?? 0) + 1);

See the documentation for the full API reference, React integration, and caveats.

See also

Development

See the roadmap for planned features, the API reference for current behavior, and how it works for design explanations.

This project uses Vite+ with the Node.js version in .node-version and the package manager declared in package.json.

vp install
vp check
vp test run
vp run test:coverage # enforces 100% runtime coverage
vp pack

Use vp pack or vp run build to build this library, including ESM, CJS, and TypeScript declarations. vp build runs Vite's application build and expects an HTML entry point. Formatting, linting, and packaging options live in vite.config.ts.

License

MIT

ultrastorage (yangshun/ultrastorage)

Searching Database…

What is this?A TypeScript library that supercharges localStorage: it serializes rich data types and adds key expiration, namespacing, subscriptions, and schema validation on top of the browser's default storage.
Is this a library I bundle into my frontend app, or some kind of hosted storage service?It's a client-side library you bundle into your own JavaScript or TypeScript code - nothing is hosted or installed on a server. It ships tree-shakeable ESM and CJS builds with full TypeScript types.
How do I add it to a project?Install the ultrastorage package from npm and follow the getting-started guide at ultrastorage.dev. One catch: it was previously published as greatstorage, so existing users should follow the migration guide.
Does it lock me into React or any particular framework?No. The core is plain, framework-agnostic TypeScript; React support is an optional ultrastorage/react adapter with typed values, functional updates, and server rendering. Schema libraries like Zod are optional too.
Any gotchas worth knowing before adopting it?Expiration is lazy by default: entries vanish only on read or when clearExpired() runs. Change events describe what changed but do not carry old or new values, and expiry timers may run late.
Can I use sessionStorage or an in-memory store instead of localStorage?Yes - pass localStorage, sessionStorage, or any object implementing the Storage interface. createMemoryStorage() provides an in-memory backend meant for tests and server-side rendering.
Can I keep devalue out of my bundle?Yes. Import from ultrastorage/core instead of the main entry - the only difference is that the serializer option becomes required, and devalue stays out of your bundle entirely.
Are there helpers for common init and update flows?Yes. appStorage.getOrInit() returns the existing value or creates, stores, and returns a new one when the key is missing, and appStorage.updateItem() reads, updates, and writes in one call.
What similar libraries does the author point to?The README's see-also section lists store2, store.js, unstorage, storage-box, and lscache - other localStorage wrappers with features like namespacing, TTL, and pluggable drivers.
Creator
yangshun
Size
0.6 MB
Stars
164
Age
7mo
Commits · 90 Days