Skip to content
ultrastorageultrastorage
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Was this page helpful?