Skip to content

Persist Data Locally

This guide covers the @parity/product-sdk-local-storage package, which provides a LocalKvStore for storing and retrieving key-value data between sessions. Use it for device-local data like saved preferences, unsubmitted drafts, or cached responses. Data is scoped to your Product and device; it does not sync across devices or leak between Products on the same Host.

Before getting started, ensure you have:

  • A Polkadot Product project running locally (see Set Up Your Project)
  • Node.js 20 or later with ESM support (@parity/product-sdk-local-storage is ESM only)

The SDK installs two ways. The umbrella package, @parity/product-sdk, is the recommended starting point: one dependency that re-exports most of the SDK. Individual packages install only what you use, which keeps your bundle smaller and your dependencies explicit, so switch to them later as a bundle-size optimization. The two use different import specifiers, so switching styles means updating your imports. See Umbrella or Individual Packages for the full comparison.

  • Umbrella package: The whole SDK in one dependency.

    Terminal window
    npm install @parity/product-sdk
  • Individual package: Only local storage.

    Terminal window
    npm install @parity/product-sdk-local-storage

The import specifiers differ between the two. The snippets in this guide use the standalone specifier @parity/product-sdk-local-storage; on the umbrella, the same exports come from the @parity/product-sdk/local-storage subpath. The React hooks shown later are umbrella-only, under @parity/product-sdk/react.

createLocalKvStore() is an async factory. You must await it before performing any read or write, because the function connects to the Host storage backend before the store is usable.

import { createLocalKvStore } from '@parity/product-sdk-local-storage';
import type { LocalKvStore } from '@parity/product-sdk-local-storage';
// Connects to the Host storage backend. Must run inside a Polkadot Product;
// outside a host container this throws "Host storage unavailable".
const store: LocalKvStore = await createLocalKvStore();

createLocalKvStore() accepts an optional LocalKvStoreOptions argument. The supported option is prefix, which is covered in Namespace Keys with Prefixes.

Use store.set(key, value) to write a string value for a key. Use store.get(key) to read it back. If the key has not been set, get returns null.

await store.set('network', 'paseo');
const network = await store.get('network');
if (network === null) {
console.log('Key not found');
} else {
console.log('Network:', network);
}

Use store.setJSON(key, value) to write any JSON-serializable value. The store handles serialization for you. Use store.getJSON<T>(key) to read it back; the generic type parameter T enables type-safe retrieval. If the key does not exist, getJSON returns null.

interface UserPreferences {
theme: 'light' | 'dark';
language: string;
}
await store.setJSON('preferences', {
theme: 'dark',
language: 'en',
} satisfies UserPreferences);
const preferences = await store.getJSON<UserPreferences>('preferences');
if (preferences === null) {
console.log('No preferences found');
} else {
console.log('Theme:', preferences.theme);
console.log('Language:', preferences.language);
}

Use store.remove(key) to delete a key. The method does not throw if the key does not exist, so you can call it safely without first checking whether the value is present.

await store.remove('network');

Within a Product, multiple features may share the same LocalKvStore. Prefixes partition the namespace further to avoid key collisions between those features.

Pass a prefix option to createLocalKvStore() to prepend prefix: to every key operated on by that store instance. For example, createLocalKvStore({ prefix: 'feature' }) stores the key 'setting' internally as 'feature:setting'.

import { createLocalKvStore } from '@parity/product-sdk-local-storage';
const settingsStore = await createLocalKvStore({ prefix: 'settings' });
const cacheStore = await createLocalKvStore({ prefix: 'cache' });
// Stored internally as "settings:theme"
await settingsStore.set('theme', 'dark');
// Stored internally as "cache:lastBlock"
await cacheStore.set('lastBlock', '21540000');

The Host-enforced Product-level namespace is separate from any developer-defined prefix. The Host's Product namespace is applied on top of your prefix, so a key 'setting' in a { prefix: 'feature' } store ends up stored as something like 'myproduct.dot:feature:setting', without you needing to construct that path yourself.

If your Product is a React application, @parity/product-sdk provides hooks that integrate local storage directly into the React component lifecycle. The hooks read the current value on mount and re-render the component whenever it changes.

Wrap your application in ProductSDKProvider once at the root, then call the hooks anywhere inside the tree:

'use client';
import { ProductSDKProvider } from '@parity/product-sdk/react';
import type { ReactNode } from 'react';
export function Providers({ children }: { children: ReactNode }) {
return (
<ProductSDKProvider
fallback={<div>Initializing SDK…</div>}
>
{children}
</ProductSDKProvider>
);
}

useLocalStorageString manages a plain string value. useLocalStorage<T> manages a JSON-serializable value typed by T. Both hooks return a tuple of [currentValue, setValue, { loading, error }]:

'use client';
import {
useLocalStorage,
useLocalStorageString,
} from '@parity/product-sdk/react';
interface UserPreferences {
theme: 'light' | 'dark';
language: string;
}
function StorageDemo() {
const [network, setNetwork, { loading: networkLoading }] =
useLocalStorageString('network');
const [
preferences,
setPreferences,
{ loading: prefsLoading, error: prefsError },
] = useLocalStorage<UserPreferences>('preferences');
return (
<div>
<p>Network: {networkLoading ? 'loading…' : (network ?? 'not set')}</p>
<button onClick={() => setNetwork('paseo')}>Set network</button>
<p>
Theme: {prefsLoading ? 'loading…' : (preferences?.theme ?? 'not set')}
</p>
<button onClick={() => setPreferences({ theme: 'dark', language: 'en' })}>
Set preferences
</button>
{prefsError && <p>Error: {prefsError.message}</p>}
</div>
);
}

To remove a key, access app.localStorage from useProductSDK() directly:

'use client';
import { useProductSDK } from '@parity/product-sdk/react';
function StorageActions() {
const app = useProductSDK();
async function handleRemove() {
await app.localStorage.remove('network');
}
return (
<div>
<button onClick={handleRemove}>Remove &quot;network&quot;</button>
</div>
);
}
  • Storage is not synced across devices. Values written on one Host instance are not visible on another.
  • app.localStorage and every LocalKvStore are scoped to your Product. You cannot read or modify another Product's keys.
  • React hooks (useLocalStorage, useLocalStorageString) are only available via the umbrella package @parity/product-sdk. The standalone @parity/product-sdk-local-storage package exposes no React hooks.
  • createLocalKvStore() requires a Host backend. Running outside a host container, it throws Host storage unavailable; there is no browser localStorage fallback. Run your Product inside Polkadot Desktop to use local storage. See Host storage unavailable in troubleshooting.
  • Store Data on Chain


    When data must outlive the device and be fetchable by anyone, move it to the Bulletin Chain.

    → Store Data on Chain

  • Deploy Your Product


    You've covered the capabilities; ship your Product to a live .dot name with the Quick Start deploy routes.

    → Quick Start

  • Product SDK API Reference


    The full product-sdk surface beyond this recipe: every package, class, and method.

    → Visit Site

Last updated: