Svelte 5 persisted states, svelte-persisted-store, but implemented with Svelte 5 Runes.
This package requires Svelte 5. It is not compatible with Svelte 4 or earlier versions.
npm install svelte-persisted-state
This package exports two functions:
persistedState - Synchronous storage (localStorage, sessionStorage, cookies)persistedStateAsync - Asynchronous storage (IndexedDB) for large datasetsThe persistedState function creates a persisted state that automatically syncs with local storage, session storage, or browser cookies.
key: A string key used for storage.initialValue: The initial value of the state.options: An optional object with the following properties:storage: 'local' (default), 'session', or 'cookie'serializer: Custom serializer object with parse and stringify methods (default: JSON)syncTabs: Boolean to sync state across tabs (default: true, only works with localStorage)cookieOptions: Cookie-specific configuration object (only applies when storage is 'cookie'):expireDays: Number of days before cookie expires (default: 365, max: 400 due to browser limits)maxAge: Max-Age in seconds (takes precedence over expireDays if both are specified)path: Cookie path (default: '/')domain: Cookie domain (default: current domain)secure: Secure flag - cookie only sent over HTTPS (default: false)sameSite: SameSite attribute for CSRF protection - 'Strict' | 'Lax' | 'None' (default: 'Lax')httpOnly: HttpOnly flag - prevents client-side script access (default: false)onWriteError: Function to handle write errorsonParseError: Function to handle parse errorsbeforeRead: Function to process value before readingbeforeWrite: Function to process value before writingThe persistedState function returns an object with the following properties:
current: Get or set the current state value.reset(): Reset the state to its initial value.import { persistedState } from 'svelte-persisted-state';
const myState = persistedState('myKey', 'initialValue');
// Use myState.current to get or set the state
console.log(myState.current);
myState.current = 'newValue';
// Reset to initial value
myState.reset();
import { persistedState } from 'svelte-persisted-state';
interface UserPreferences {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
}
const userPreferences = persistedState<UserPreferences>(
'userPreferences',
{
theme: 'light',
fontSize: 16,
notifications: true
},
{
storage: 'local',
syncTabs: true,
beforeWrite: (value) => {
console.log('Saving preferences:', value);
return value;
},
onWriteError: (error) => {
console.error('Failed to save preferences:', error);
}
}
);
function toggleTheme() {
userPreferences.current.theme = userPreferences.current.theme === 'light' ? 'dark' : 'light';
}
// Using $derived for reactive computations
const theme = $derived(userPreferences.current.theme);
// The UI will automatically update when the state changes
JSON can't natively serialize objects like Map, Set, Date, BigInt, or circular references. To persist such values, pass a custom serializer. A simple and reliable option is devalue.
Install devalue:
npm install devalue
Use it as the serializer:
import { persistedState } from 'svelte-persisted-state';
import * as devalue from 'devalue';
const devalueSerializer = {
stringify: devalue.stringify,
parse: devalue.parse
};
// Works with Maps, Sets, Dates, nested structures, etc.
export const complexData = persistedState(
'complexData',
{
name: 'Example',
created: new Date(),
nested: {
array: [1, 2, 3],
map: new Map([
['key1', 'value1'],
['key2', 'value2']
]),
set: new Set([1, 2, 3])
}
},
{
serializer: devalueSerializer
}
);
// Tip: if you mutate a Map/Set in-place, reassign to trigger reactivity:
// complexData.current.nested.map.set('key3', 'value3');
// complexData.current = { ...complexData.current };
You can use cookies for storage, which is useful for SSR scenarios or when you need data to persist across subdomains:
import { persistedState } from 'svelte-persisted-state';
const cookieState = persistedState('myCookieKey', 'defaultValue', {
storage: 'cookie',
cookieOptions: {
expireDays: 30 // Custom expiration
}
});
Notes:
syncTabs doesn’t work with cookiesFor large datasets (50MB+) or async usage, use persistedStateAsync with IndexedDB:
import { persistedStateAsync } from 'svelte-persisted-state';
const largeData = persistedStateAsync('large-dataset', [], {
indexedDB: {
dbName: 'my-app', // default: 'svelte-persisted-state'
storeName: 'state', // default: 'state'
version: 1 // default: 1
},
syncTabs: true, // Uses BroadcastChannel for cross-tab sync
onHydrated: (value) => console.log('Data loaded:', value.length)
});
Unlike persistedState which uses JSON serialization by default, persistedStateAsync uses IndexedDB's native Structured Clone Algorithm. This means you can store complex JavaScript types directly without a custom serializer:
// Date, Map, Set, RegExp, TypedArrays work out of the box!
const appState = persistedStateAsync('app-state', {
lastVisit: new Date(),
userTags: new Set(['svelte', 'typescript']),
preferences: new Map([
['theme', 'dark'],
['language', 'en']
]),
pattern: /search-\d+/gi,
buffer: new Uint8Array([1, 2, 3, 4])
});
// No serializer needed - values are stored natively
await appState.ready;
console.log(appState.current.lastVisit instanceof Date); // true
console.log(appState.current.userTags instanceof Set); // true
Benefits over JSON:
Date, Map, Set, RegExp, ArrayBuffer, TypedArray, Blob, FileIf you need JSON or custom serialization, you can opt-in:
const legacyData = persistedStateAsync('legacy-key', {}, {
serializer: JSON // Opt-in to JSON (or custom) serialization
});
key: A string key used for storageinitialValue: The initial value (returned immediately, before hydration)options: An optional object with the following properties:indexedDB: IndexedDB configuration object:dbName: Database name (default: 'svelte-persisted-state')storeName: Object store name (default: 'state')version: Database version (default: 1)serializer: Custom serializer with parse and stringify methods (default: none, uses structured clone)syncTabs: Boolean to sync state across tabs via BroadcastChannel (default: true)onWriteError: Function to handle write errorsonParseError: Function to handle parse errors (only applies when using a serializer)onHydrated: Callback when hydration completes with the loaded valueonHydrationError: Function to handle hydration errorsbeforeRead: Function to process value before readingbeforeWrite: Function to process value before writingpersistedStateAsync returns immediately with the initial value and hydrates asynchronously in the background:
interface AsyncPersistedState<T> {
current: T; // Get or set the current value (reactive)
isLoading: boolean; // True while hydrating from IndexedDB
ready: Promise<T>; // Resolves when hydration completes
reset(): void; // Reset to initial value
}
Since current is reactive ($state), the UI automatically updates when hydration completes. No loading state handling is required if you're okay with the initial value showing briefly:
<script lang="ts">
import { persistedStateAsync } from 'svelte-persisted-state';
const notes = persistedStateAsync('notes', []);
</script>
<!-- This automatically updates when data loads from IndexedDB -->
<p>You have {notes.current.length} notes</p>
{#each notes.current as note}
<div>{note.title}</div>
{/each}
<button onclick={() => notes.current = [...notes.current, { title: 'New' }]}>
Add Note
</button>
If you want to show a loading indicator while hydrating, use isLoading or {#await}:
<!-- Using isLoading -->
{#if data.isLoading}
<Spinner />
{:else}
<List items={data.current} />
{/if}
<!-- Using {#await} -->
{#await data.ready}
<p>Loading...</p>
{:then}
<List items={data.current} />
{:catch error}
<p>Error: {error.message}</p>
{/await}
const data = persistedStateAsync('my-data', []);
// Optionally wait for hydration
await data.ready;
console.log('Hydrated:', data.current);
// Or capture the hydrated value directly
const value = await data.ready;
console.log('Hydrated:', value);
For TypeScript users, the following types are exported:
import type {
AsyncOptions,
AsyncPersistedState,
IndexedDBOptions
} from 'svelte-persisted-state';
| Feature | localStorage | sessionStorage | cookies | IndexedDB |
|---|---|---|---|---|
| Persistence | Until manually cleared | Until tab/window closes | Until expiration date | Until manually cleared |
| Size Limit | ~5-10MB | ~5-10MB | ~4KB | ~50MB+ (browser dependent) |
| API Type | Sync | Sync | Sync | Async |
| Server Access | No | No | Yes (sent with requests) | No |
| Tab Sync | Yes (with syncTabs: true) |
No | No | Yes (via BroadcastChannel) |
| SSR Compatible | No | No | Yes | No |
| Expiration | Manual | Automatic | Configurable | Manual |
<script lang="ts">
import { persistedState } from 'svelte-persisted-state';
interface UserPreferences {
theme: 'light' | 'dark';
fontSize: number;
}
const preferences = persistedState<UserPreferences>('preferences', {
theme: 'light',
fontSize: 16
});
const theme = $derived(preferences.current.theme);
const fontSize = $derived(preferences.current.fontSize);
</script>
<div style="font-size: {fontSize}px">
<button onclick={() => (preferences.current.theme = theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
<button onclick={() => (preferences.current.fontSize -= 1)}> Decrease font size </button>
<button onclick={() => (preferences.current.fontSize += 1)}> Increase font size </button>
<p>Current theme: {theme}</p>
<p>Current font size: {fontSize}px</p>
</div>
<script lang="ts">
import { persistedState } from 'svelte-persisted-state';
// User session data stored in cookies (expires in 30 days)
const userSession = persistedState(
'user-session',
{
isLoggedIn: false,
username: ''
},
{
storage: 'cookie',
cookieOptions: {
expireDays: 30
}
}
);
// Shopping cart stored in cookies (expires in 7 days)
const cart = persistedState('shopping-cart', [], {
storage: 'cookie',
cookieOptions: {
expireDays: 7
}
});
function login(username: string) {
userSession.current = { isLoggedIn: true, username };
}
function logout() {
userSession.current = { isLoggedIn: false, username: '' };
}
</script>
{#if userSession.current.isLoggedIn}
<p>Welcome back, {userSession.current.username}!</p>
<button onclick={logout}>Logout</button>
{:else}
<button onclick={() => login('demo-user')}>Login as Demo User</button>
{/if}
<p>Cart items: {cart.current.length}</p>
MIT