Library / SDK
joshnuss/svelte-persisted-store avatar
joshnuss/svelte-persisted-store

svelte-persisted-store: a Svelte store that survives a reload

A Svelte store that persists to localStorage

1,161 stars40 forksTypeScriptMIT

At a glance

What is it?
svelte-persisted-store is a small Svelte store that writes its value to local storage and reads it back, with cross tab synchronisation, pluggable serialisation, session storage support and hooks for intercepting reads and writes. It is a single function with an options object, published on npm, MIT licensed, and built with tsup and released with semantic-release.
Who is it for?
Adopt svelte-persisted-store if you have a Svelte application with a small amount of client state that should survive a reload, such as a theme, a panel width or a set of preferences, and you want cross tab consistency without wiring storage events yourself.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 9 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One function that returns a store

svelte-persisted-store is a Svelte store that persists to local storage and can sync changes across browser tabs. The whole idea fits in one call, where the first parameter is the local storage key and the second is the initial value:

javascript
import { persisted } from 'svelte-persisted-store'

// First param `preferences` is the local storage key.
// Second param is the initial value.
export const preferences = persisted('preferences', {
  theme: 'dark',
  pane: '50%',
  ...
})

What you get back behaves like any other Svelte store, which is the point of the design. You subscribe to changes, you update the value, and you set it, and because it is a standard store contract the familiar helpers apply, including `get(preferences)` to read a value outside a component and the `$preferences` auto-subscription syntax inside one. On top of the standard contract it adds a `reset()` method that returns the store to the initial value you declared, which is the piece a hand rolled persistence layer usually misses, since resetting to a default is a requirement in any settings screen with a revert button. The store is also published on npm with a dependents graph linked from the README, and the project carries a codecov badge for coverage.

Options: serializer, storage target, tab sync and error hooks

The third parameter is an options object, and it is where the real behaviour lives.

javascript
// third parameter is options
export const preferences = persisted('local-storage-key', 'default-value', {
  serializer: devalue, // defaults to `JSON`
  storage: 'session', // 'session' for sessionStorage, defaults to 'local'
  syncTabs: true, // choose whether to sync localStorage across tabs, default is true
  onWriteError: (error) => {/* handle or rethrow */}, // Defaults to console.error with the error object
  onParseError: (raw, error) => {/* handle or rethrow */}, // Defaults to console.error with the error object
  beforeRead: (value) => {/* change value after serialization but before setting store to return value*/},
  beforeWrite: (value) => {/* change value after writing to store, but before writing return value to local storage*/},
})

Four of these are straightforward. The serializer defaults to `JSON`, and the README's example shows how to swap in a different one by passing a module such as `devalue`. The `storage` option chooses between local storage and session storage, with `session` selecting sessionStorage and the default being local. `syncTabs` decides whether changes are synchronised across browser tabs and defaults to true. The error hooks are the thoughtful part: `onWriteError` and `onParseError` both default to logging the error object with `console.error`, and both let you handle or rethrow, which means a quota exceeded error on write can be handled rather than silently swallowed. The two `before` hooks are the ones to read twice, because their names are easy to misjudge. `beforeRead` changes the value after deserialisation but before the store is set to it, so it is a normalising hook for what comes out of storage, and `beforeWrite` changes the value after it has been written to the store but before it is written to local storage, so it is a sanitising hook for what goes in.

Cross tab sync is on by default, and the mechanism is undocumented

The headline feature beyond persistence itself is that changes can be synced across browser tabs, and the default for that is enabled. In practice that means a user with the same application open in two windows sees the same preference value in both, which is the behaviour people expect from desktop software and usually do not get from a naive local storage wrapper. What the README does not say is how it works, and the repository topics include pubsub, which points at an event fan out rather than a polling loop, but the transport is not described anywhere in the documentation. That is a fair thing to note for a reader who needs to reason about it, because the mechanism determines the latency of the sync, whether a change made in one tab is visible immediately in another, and what happens if a tab is backgrounded. If your application depends on cross tab state, the practical way to find out is to open two tabs and watch. The library is a store, so a subscription in the second tab will fire when the first tab writes, and that is as much as the documentation promises.

The build and release pipeline is modern and small

The package manifest shows how the library is produced. The build script runs `tsup index.ts --format esm,cjs --sourcemap --dts`, so the single `index.ts` source is compiled into both ES module and CommonJS output with source maps and type declarations, and the published `files` list contains only `dist`. The `prepare` script runs that build, so an install from git gets a working package, and `prepublishOnly` runs the tests and the lint first, which is a sensible order for a package that publishes from a script. Versions are handled by semantic-release, and the manifest carries the placeholder version `0.0.0-development`, which is the convention for that setup, so the real version comes from the release tags rather than from a file a maintainer edits. Testing uses vitest with the jsdom environment, since the code touches browser storage, and coverage goes to codecov. Linting is eslint with a separate format script that applies the fixes. The repository also pins tool versions in a `.tool-versions` file and keeps a pnpm lockfile, and the `publint` dependency is listed in the manifest.

Svelte versions, the Node constraint, and the gaps in the documentation

The compatibility story is declared in two places. The peer dependency is `svelte` with the range `^3.48.0 || ^4 || ^5`, so the library works across Svelte 3, 4 and 5, while the development dependency is Svelte 4, which is what the tests actually run against, so the Svelte 3 and 5 ends of that range are supported by declaration rather than by the test suite in this repository. The `engines` field says `node >=0.14`, which is a very loose constraint for a package that ships as an ES module with a `.d.ts`, and it is best read as a floor rather than a real support statement. Two documentation gaps are worth flagging for an evaluator. The README does not describe how the store behaves during server-side rendering, where local storage does not exist, and it does not document what happens to the stored value if the key is absent, if the value is of a different shape than the initial value, or if the schema changes between versions of your application. The error hooks give you the hooks to handle the second and third of those yourself, which is the right design, but the default behaviour of quietly logging and continuing is a choice you should make consciously rather than inherit.

Editorial conclusion

Adopt svelte-persisted-store if you have a Svelte application with a small amount of client state that should survive a reload, such as a theme, a panel width or a set of preferences, and you want cross tab consistency without wiring storage events yourself. Do not adopt it for large or sensitive state, since the whole value is serialised to a single storage key, and do not adopt it in a server rendered application without checking the behaviour yourself, because the README does not document what the store does during server-side rendering. Before you ship, decide what happens when the stored value cannot be parsed, since the default is to log to the console and carry on, and pick a serializer if your state is not plain JSON. The licence is MIT, the latest published version is 0.12.0 from 2024-10-31, and the last push was on 2026-10-16.

Frequently asked questions

How do I create a persisted store with svelte-persisted-store?

Call `persisted` with a local storage key and an initial value, for example `persisted('preferences', { theme: 'dark' })`. The returned value is a normal Svelte store, so you can subscribe, update and set it, use `get(store)` or the `$store` syntax to read it, and call `reset()` to return it to the initial value.

Can svelte-persisted-store store values that are not plain JSON?

Yes, by passing a different serializer in the options, since the serializer defaults to JSON. The README shows an example that imports `devalue` and passes it as the `serializer` option.

How does svelte-persisted-store handle storage errors?

The `onWriteError` and `onParseError` options receive the error, and both default to logging it with `console.error` and the error object. You can handle the error or rethrow it, so a quota failure on write does not have to pass silently.

Does svelte-persisted-store synchronise between browser tabs?

Yes, and it is on by default, with a `syncTabs` option to turn it off. The README states that the library can sync changes across browser tabs but does not document the mechanism, so the exact propagation behaviour is something to verify in your own application.

Which Svelte versions does svelte-persisted-store support?

The peer dependency declares `svelte` as `^3.48.0 || ^4 || ^5`, so Svelte 3, 4 and 5 are supported, while the development dependency, and therefore the test suite, runs against Svelte 4. The package is published on npm as `svelte-persisted-store` under the MIT licence.

Official sources

  1. Issues
  2. joshnuss/svelte-persisted-store on GitHub
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/joshnuss-svelte-persisted-store.svg)](https://hysenlabs.com/projects/joshnuss-svelte-persisted-store)

Was this useful?