---
title: Persist Data Locally
description: Learn how to store and retrieve application data between sessions in a Polkadot Product using the @parity/product-sdk-local-storage package.
categories: Apps
url: https://developer.polkadot.com/apps/build/persist-data-locally/
word_count: 1535
token_estimate: 2828
version_hash: sha256:0b4c71ce4a5005cecb96c2b665a12dc08dbec67142a88056585f2b8c1c33da10
last_updated: 2026-10-28T08:43:55+00:00
---

# Persist Data Locally

## Introduction

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.

!!! info "Storage options for your Product"
    Match the data to the layer built for it; see [Where to Store Data](/apps/concepts/data-placement/) for the full decision guide.

    - **Local storage**: Per-Product, per-device key-value for preferences, drafts, and cached values. Not synced across devices. See [Persist Data Locally](/apps/build/persist-data-locally/).
    - **Bulletin Chain**: Content-addressed, on-chain, retained ~2 weeks by default and renewable. Fetched later by CID: profile photos, published articles, app bundles. See [Store Data on Chain](/apps/build/store-data-on-chain/).
    - **Statement Store**: Gossip-distributed, short-lived (default 30s TTL), allowance-gated. Real-time signaling between users: chat, presence, typing indicators. See [Publish and Subscribe to Off-Chain Data](/apps/build/pub-sub-off-chain-data/).


## Prerequisites

Before getting started, ensure you have:

- A Polkadot Product project running locally (see [Set Up Your Project](/apps/build/#set-up-your-project))
- Node.js 20 or later with ESM support (`@parity/product-sdk-local-storage` is ESM only)

## Install the SDK

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](/apps/build/#umbrella-or-individual-packages) for the full comparison.


- **Umbrella package**: The whole SDK in one dependency.

    ```bash
    npm install @parity/product-sdk
    ```

- **Individual package**: Only local storage.

    ```bash
    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`.

!!! note "Code examples"
    Each snippet in this guide is a standalone file you add to your Product's source tree. The filenames match the `title` shown in the code block header (for example, `initialize.ts`, `set-get-string.ts`). They are not meant to be concatenated; import and use each one independently wherever it fits in your Product.

## Initialize the Store

`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.

```typescript title="initialize.ts"
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](#namespace-keys-with-prefixes).

## Store String Values

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`.

```typescript title="set-get-string.ts"
await store.set('network', 'paseo');

const network = await store.get('network');

if (network === null) {
  console.log('Key not found');
} else {
  console.log('Network:', network);
}

```

## Store JSON Values

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`.

```typescript title="set-get-json.ts"
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);
}

```

## Remove Stored Values

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.

```typescript title="remove.ts"
await store.remove('network');

```

## Namespace Keys with Prefixes

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'`.

```typescript title="prefixed-store.ts"
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.

!!! warning "Prefixed stores do not share a key space"
    `app.localStorage` is itself a prefixed store: `createApp` builds it with a prefix taken from the Host's product ID, minus the final domain suffix (`my-product.dot` becomes `my-product`). Before v0.30.0, the prefix is the `name` you passed. A store you create with a different `prefix` writes under a different path, so a key written through `createLocalKvStore({ prefix: 'feature' })` and read back through `app.localStorage` resolves to `null` rather than raising an error. Read and write each key through the same store.

## Use React Hooks

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:

```tsx title="providers.tsx"
'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 }]`:

```tsx title="local-storage-hooks.tsx"
'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:

```tsx title="remove-key.tsx"
'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>
  );
}

```

!!! note "There is no clear-all"
    `LocalStorageApi` exposes `get`, `getJSON`, `set`, `setJSON`, and `remove`, and nothing else. The backing store has no way to enumerate keys, so removing everything your Product wrote means calling `remove()` on each key you know about. Track the keys yourself if you need that.

## Limitations

- 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](/reference/apps/hosts/polkadot-desktop/) to use local storage. See [`Host storage unavailable`](/apps/troubleshooting/#uploads-are-rejected-or-host-storage-unavailable) in troubleshooting.

## Where to Go Next

<div class="grid cards" markdown>

-   <span class="badge guide">Guide</span> **Store Data on Chain**

    ---

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

    [:octicons-arrow-right-24: Store Data on Chain](/apps/build/store-data-on-chain/)

-   <span class="badge guide">Guide</span> **Deploy Your Product**

    ---

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

    [:octicons-arrow-right-24: Quick Start](/apps/quick-start/)

-   <span class="badge external">External</span> **Product SDK API Reference**

    ---

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

    [:octicons-arrow-right-24: Visit Site](https://paritytech.github.io/product-sdk/)

</div>
