Svelte 5 persisted states, [svelte-persisted-store](https://github.com/joshnuss/svelte-persisted-store), but implemented with Svelte 5 Runes.
npm install svelte-persisted-stateSvelte 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.
``bash`
npm install svelte-persisted-state
This package exports two functions:
- persistedState - Synchronous storage (localStorage, sessionStorage, cookies)persistedStateAsync
- - Asynchronous storage (IndexedDB) for large datasets
The 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 writing
The 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.
`javascript
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();
`
`typescript
import { persistedState } from 'svelte-persisted-state';
interface UserPreferences {
theme: 'light' | 'dark';
fontSize: number;
notifications: boolean;
}
const userPreferences = persistedState
'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:
`bash`
npm install devalue
Use it as the serializer:
`typescript
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:
`typescript
import { persistedState } from 'svelte-persisted-state';
const cookieState = persistedState('myCookieKey', 'defaultValue', {
storage: 'cookie',
cookieOptions: {
expireDays: 30 // Custom expiration
}
});
`
Notes:
- Cookies have a size limit (~4KB per cookie)
- syncTabs doesn’t work with cookies
- Cookies are sent with every HTTP request
- Modern browsers cap expiration at about 400 days
For large datasets (50MB+) or async usage, use persistedStateAsync with IndexedDB:
`typescript
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)
});
`
#### Native Type Support (Structured Clone)
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:
`typescript
// 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:
- Native support for Date, Map, Set, RegExp, ArrayBuffer, TypedArray, Blob, File
- Handles circular references
- No serialization/deserialization overhead
- Better performance for large objects
If you need JSON or custom serialization, you can opt-in:
`typescript`
const legacyData = persistedStateAsync('legacy-key', {}, {
serializer: JSON // Opt-in to JSON (or custom) serialization
});
#### Parameters
- 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 writing
#### Return Value
persistedStateAsync returns immediately with the initial value and hydrates asynchronously in the background:
`typescript`
interface AsyncPersistedState
current: T; // Get or set the current value (reactive)
isLoading: boolean; // True while hydrating from IndexedDB
ready: Promise
reset(): void; // Reset to initial value
}
#### Automatic Reactivity (Simplest Usage)
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:
`svelte
You have {notes.current.length} notes
{#each notes.current as note}
`
#### Optional: Loading States
If you want to show a loading indicator while hydrating, use isLoading or {#await}:
`svelte
{#if data.isLoading}
{:else}
{/if}
{#await data.ready}
Loading...
Error: {error.message}
#### Awaiting in JavaScript
`typescript
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);
`$3
For TypeScript users, the following types are exported:
`typescript
import type {
AsyncOptions,
AsyncPersistedState,
IndexedDBOptions
} from 'svelte-persisted-state';
`$3
| 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 |Examples
$3
`svelte
Current theme: {theme}
Current font size: {fontSize}px
`$3
`svelte
{#if userSession.current.isLoggedIn}
Welcome back, {userSession.current.username}!
{:else}
{/if}Cart items: {cart.current.length}
``MIT