Destinations and serializers
Choose where values are saved and how they are encoded.
A destination is where ultrastorage saves values. Persistent browser data may belong in
localStorage, tab-specific data in sessionStorage, and test or server data in memory.
You may also need a particular serialization format. ultrastorage lets you configure both while keeping the same API.
Choose a destination
Pass localStorage, sessionStorage, or any object that implements Storage through the
storage option. Use createMemoryStorage() for tests and server-side rendering.
import { createStorage, createMemoryStorage } from 'ultrastorage';
export const appStorage = createStorage({
prefix: 'acme',
storage: typeof window === 'undefined' ? createMemoryStorage() : undefined,
});
Save values in memory
createMemoryStorage() returns an in-memory Storage implementation for tests or server-side
usage.
import { createStorage, createMemoryStorage } from 'ultrastorage';
const memory = createMemoryStorage();
export const appStorage = createStorage({ prefix: 'acme', storage: memory });
Use a separate in-memory object for each server request when saving request-specific data. Separate
instances must use the same object through the storage option to receive each other’s local
notifications.
Custom serialization
Don’t like devalue? Bring your own stringify/parse and we won’t judge. Much.
import { createStorage } from 'ultrastorage';
import superjson from 'superjson';
export 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';
export const appStorage = createStorage({
prefix: 'acme',
serializer: { stringify: superjson.stringify, parse: superjson.parse },
});
Changing serializers can make existing entries unreadable. Prefer a new prefix when changing formats and explicitly migrate any values you need to retain.
A custom serializer must synchronously encode and decode the entire storage envelope, including its value, marker, version, and expiration metadata. Its supported value types determine what you can store reliably.
If parse() or an explicit fallbackParser returns a promise or thenable (an object or function
with a callable then method), ultrastorage throws a synchronous TypeError and consumes any
rejection. An async primary parser does not trigger fallback.
For example, plain JSON does not preserve Map, Set, or Date types, and drops object properties
whose value is undefined. Storing undefined as the entry value drops the envelope’s required
value property:
import { createStorage } from 'ultrastorage';
export const appStorage = createStorage({ prefix: 'acme', serializer: JSON });
import { appStorage } from './lib/app-storage';
appStorage.setItem('optional', undefined);
appStorage.getItem('optional'); // null: the stored envelope is not recognized
appStorage.has('optional'); // false
Explicit fallback parser
Reads use only the configured parser by default, including the default devalue parser.
If it throws, getItemResult() returns parse-error with that error and getItem() returns null.
To support another stored format, provide a synchronous fallbackParser:
import { createStorage } from 'ultrastorage';
export const appStorage = createStorage({
prefix: 'acme',
fallbackParser: JSON.parse,
});
fallbackParser is also available with custom serializers and from ultrastorage/core.
It receives the same raw string and runs only when the primary parser throws synchronously.
A primary parser that returns an unrecognized envelope produces unsupported without trying
fallback. If both parsers throw, parse-error retains the primary parser’s error; an unrecognized
fallback result produces unsupported.
The same policy applies to reads, expiration metadata, enumeration, clearing, React snapshots,
and reactive expiration. Writes always use serializer.stringify(). Reading through a fallback
does not migrate the stored format; normal expiration cleanup still applies.
Both paths still require a recognized storage envelope; fallback does not make raw strings or ordinary JSON values readable. Use explicit migration for those values.
Earlier versions automatically tried JSON after parser errors. See the upgrade guidance to retain that behavior.
Entries left in an unreadable format still occupy backend storage, and clear() cannot remove
them. Keep access to the old serializer while migrating, or remove known obsolete keys explicitly;
see clearing unreadable entries.