├── .npmrc
├── src
├── app.css
├── routes
│ ├── +layout.ts
│ ├── +layout.svelte
│ ├── page.svelte.test.ts
│ └── +page.svelte
├── lib
│ ├── index.ts
│ ├── new.svelte.ts
│ └── storage.svelte.ts
├── demo.spec.ts
├── app.html
└── app.d.ts
├── .prettierignore
├── static
└── favicon.png
├── tsconfig.tsbuildinfo
├── .prettierrc
├── .gitignore
├── tsconfig.json
├── vitest-setup-client.ts
├── svelte.config.js
├── eslint.config.js
├── vite.config.ts
├── LICENSE
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | export const ssr = false;
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/friendofsvelte/state/HEAD/static/favicon.png
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | // Reexport your entry components here
2 | export { PersistentState } from './storage.svelte.js';
3 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | {@render children()}
7 |
--------------------------------------------------------------------------------
/src/demo.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 |
3 | describe('sum test', () => {
4 | it('adds 1 + 2 to equal 3', () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/lib/new.svelte.ts:
--------------------------------------------------------------------------------
1 | import { PersistentState } from "./storage.svelte.js";
2 |
3 | export const box = new PersistentState('box', {
4 | color: '#ff3e00',
5 | dimensions: [100, 100]
6 | }, 'sessionStorage');
--------------------------------------------------------------------------------
/tsconfig.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./.svelte-kit/ambient.d.ts","./.svelte-kit/non-ambient.d.ts","./.svelte-kit/types/src/routes/$types.d.ts","./vite.config.ts","./src/app.d.ts","./src/demo.spec.ts","./src/lib/index.ts","./src/lib/state.svelte.ts","./src/routes/page.svelte.test.ts"],"version":"5.7.3"}
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "overrides": [
8 | {
9 | "files": "*.svelte",
10 | "options": {
11 | "parser": "svelte"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | .netlify
7 | .wrangler
8 | .idea
9 | /.svelte-kit
10 | /build
11 | /dist
12 |
13 | # OS
14 | .DS_Store
15 | Thumbs.db
16 |
17 | # Env
18 | .env
19 | .env.*
20 | !.env.example
21 | !.env.test
22 |
23 | # Vite
24 | vite.config.js.timestamp-*
25 | vite.config.ts.timestamp-*
26 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/routes/page.svelte.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, test, expect } from 'vitest';
2 | import '@testing-library/jest-dom/vitest';
3 | import { render, screen } from '@testing-library/svelte';
4 | import Page from './+page.svelte';
5 |
6 | describe('/+page.svelte', () => {
7 | test('should render h1', () => {
8 | render(Page);
9 | expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/vitest-setup-client.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest';
2 | import { vi } from 'vitest';
3 |
4 | // required for svelte5 + jsdom as jsdom does not support matchMedia
5 | Object.defineProperty(window, 'matchMedia', {
6 | writable: true,
7 | enumerable: true,
8 | value: vi.fn().mockImplementation((query) => ({
9 | matches: false,
10 | media: query,
11 | onchange: null,
12 | addEventListener: vi.fn(),
13 | removeEventListener: vi.fn(),
14 | dispatchEvent: vi.fn()
15 | }))
16 | });
17 |
18 | // add more mocks here if you need them
19 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://svelte.dev/docs/kit/types#app.d.ts
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 |
12 | interface PodTypeRegistry {
13 | layout: {
14 | bg: string;
15 | };
16 | // Add more types as needed
17 | userSettings: {
18 | theme: 'light' | 'dark';
19 | fontSize: number;
20 | };
21 | // etc...
22 | }
23 | }
24 |
25 | export {};
26 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://svelte.dev/docs/kit/integrations
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters.
14 | adapter: adapter()
15 | }
16 | };
17 |
18 | export default config;
19 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 | {box.current.color}
23 |
24 |
25 |
28 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import prettier from 'eslint-config-prettier';
2 | import js from '@eslint/js';
3 | import { includeIgnoreFile } from '@eslint/compat';
4 | import svelte from 'eslint-plugin-svelte';
5 | import globals from 'globals';
6 | import { fileURLToPath } from 'node:url';
7 | import ts from 'typescript-eslint';
8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
9 |
10 | export default ts.config(
11 | includeIgnoreFile(gitignorePath),
12 | js.configs.recommended,
13 | ...ts.configs.recommended,
14 | ...svelte.configs['flat/recommended'],
15 | prettier,
16 | ...svelte.configs['flat/prettier'],
17 | {
18 | languageOptions: {
19 | globals: {
20 | ...globals.browser,
21 | ...globals.node
22 | }
23 | }
24 | },
25 | {
26 | files: ['**/*.svelte'],
27 |
28 | languageOptions: {
29 | parserOptions: {
30 | parser: ts.parser
31 | }
32 | }
33 | }
34 | );
35 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import tailwindcss from '@tailwindcss/vite';
2 | import { svelteTesting } from '@testing-library/svelte/vite';
3 | import { sveltekit } from '@sveltejs/kit/vite';
4 | import { defineConfig } from 'vite';
5 |
6 | export default defineConfig({
7 | plugins: [sveltekit(), tailwindcss()],
8 |
9 | test: {
10 | workspace: [
11 | {
12 | extends: './vite.config.ts',
13 | plugins: [svelteTesting()],
14 |
15 | test: {
16 | name: 'client',
17 | environment: 'jsdom',
18 | clearMocks: true,
19 | include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
20 | exclude: ['src/lib/server/**'],
21 | setupFiles: ['./vitest-setup-client.ts']
22 | }
23 | },
24 | {
25 | extends: './vite.config.ts',
26 |
27 | test: {
28 | name: 'server',
29 | environment: 'node',
30 | include: ['src/**/*.{test,spec}.{js,ts}'],
31 | exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
32 | }
33 | }
34 | ]
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Friend Of Svelte
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@friendofsvelte/state",
3 | "description": "Persistent Svelte 5 State, localStorage & sessionStorage",
4 | "version": "0.0.6-ts",
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build && npm run prepack",
8 | "preview": "vite preview",
9 | "prepare": "svelte-kit sync || echo ''",
10 | "prepack": "svelte-kit sync && svelte-package && publint",
11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13 | "format": "prettier --write .",
14 | "lint": "prettier --check . && eslint .",
15 | "test:unit": "vitest",
16 | "test": "npm run test:unit -- --run"
17 | },
18 | "files": [
19 | "dist",
20 | "!dist/**/*.test.*",
21 | "!dist/**/*.spec.*"
22 | ],
23 | "sideEffects": [
24 | "**/*.css"
25 | ],
26 | "svelte": "./dist/index.js",
27 | "types": "./dist/index.d.ts",
28 | "type": "module",
29 | "exports": {
30 | ".": {
31 | "types": "./dist/index.d.ts",
32 | "svelte": "./dist/index.js"
33 | }
34 | },
35 | "peerDependencies": {
36 | "svelte": "^5.0.0"
37 | },
38 | "devDependencies": {
39 | "@eslint/compat": "^1.2.5",
40 | "@eslint/js": "^9.18.0",
41 | "@sveltejs/adapter-auto": "^4.0.0",
42 | "@sveltejs/kit": "^2.16.0",
43 | "@sveltejs/package": "^2.0.0",
44 | "@sveltejs/vite-plugin-svelte": "^5.0.0",
45 | "@tailwindcss/vite": "^4.0.0",
46 | "@testing-library/jest-dom": "^6.6.3",
47 | "@testing-library/svelte": "^5.2.4",
48 | "eslint": "^9.18.0",
49 | "eslint-config-prettier": "^10.0.1",
50 | "eslint-plugin-svelte": "^2.46.1",
51 | "globals": "^15.14.0",
52 | "jsdom": "^25.0.1",
53 | "prettier": "^3.4.2",
54 | "prettier-plugin-svelte": "^3.3.3",
55 | "prettier-plugin-tailwindcss": "^0.6.11",
56 | "publint": "^0.3.2",
57 | "svelte": "^5.0.0",
58 | "svelte-check": "^4.0.0",
59 | "tailwindcss": "^4.0.0",
60 | "typescript": "^5.0.0",
61 | "typescript-eslint": "^8.20.0",
62 | "vite": "^6.0.0",
63 | "vitest": "^3.0.0"
64 | },
65 | "repository": {
66 | "type": "git",
67 | "url": "https://github.com/friendofsvelte/state.git"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/lib/storage.svelte.ts:
--------------------------------------------------------------------------------
1 | // Based on: https://github.com/Rich-Harris/local-storage-test/blob/main/src/lib/storage.svelte.ts
2 |
3 | import { tick } from 'svelte';
4 |
5 | export type StorageType = 'localStorage' | 'sessionStorage';
6 |
7 | export class PersistentState {
8 | #key: string;
9 | #version = $state(0);
10 | #listeners = 0;
11 | #value: T | undefined;
12 | #storage: Storage;
13 |
14 | #handler = (e: StorageEvent) => {
15 | if (e.storageArea !== this.#storage) return;
16 | if (e.key !== this.#key) return;
17 |
18 | this.#version += 1;
19 | };
20 |
21 | constructor(key: string, initial?: T, storageType: StorageType = 'localStorage') {
22 | this.#key = key;
23 | this.#value = initial;
24 | this.#storage = storageType === 'localStorage' ? localStorage : sessionStorage;
25 |
26 | if (typeof this.#storage !== 'undefined') {
27 | if (this.#storage.getItem(key) === null) {
28 | this.#storage.setItem(key, JSON.stringify(initial));
29 | }
30 | }
31 | }
32 |
33 | get current(): T {
34 | this.#version;
35 |
36 | const root =
37 | typeof this.#storage !== 'undefined'
38 | ? JSON.parse(this.#storage.getItem(this.#key) as any)
39 | : this.#value;
40 |
41 | const proxies = new WeakMap();
42 |
43 | const proxy = (value: unknown) => {
44 | if (typeof value !== 'object' || value === null) {
45 | return value;
46 | }
47 |
48 | let p = proxies.get(value);
49 | if (!p) {
50 | p = new Proxy(value, {
51 | get: (target, property) => {
52 | this.#version;
53 | return proxy(Reflect.get(target, property));
54 | },
55 | set: (target, property, value) => {
56 | this.#version += 1;
57 | Reflect.set(target, property, value);
58 |
59 | if (typeof this.#storage !== 'undefined') {
60 | this.#storage.setItem(this.#key, JSON.stringify(root));
61 | }
62 |
63 | return true;
64 | }
65 | });
66 | proxies.set(value, p);
67 | }
68 |
69 | return p;
70 | };
71 |
72 | if ($effect.tracking()) {
73 | $effect(() => {
74 | if (this.#listeners === 0) {
75 | window.addEventListener('storage', this.#handler);
76 | }
77 |
78 | this.#listeners += 1;
79 |
80 | return () => {
81 | tick().then(() => {
82 | this.#listeners -= 1;
83 | if (this.#listeners === 0) {
84 | window.removeEventListener('storage', this.#handler);
85 | }
86 | });
87 | };
88 | });
89 | }
90 | return proxy(root);
91 | }
92 |
93 | set current(value: T) {
94 | if (typeof this.#storage !== 'undefined') {
95 | this.#storage.setItem(this.#key, JSON.stringify(value));
96 | }
97 |
98 | this.#version += 1;
99 | }
100 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Persistent Svelte 5 State
2 |
3 | A lightweight, type-safe state management solution for Svelte applications with built-in storage persistence.
4 |
5 | ## Features
6 |
7 | - 🎯 **Type-safe**: Full TypeScript support with automatic type inference
8 | - 💾 **Persistent Storage**: Automatic state persistence in localStorage or sessionStorage
9 | - 🪶 **Lightweight**: Zero dependencies beyond Svelte
10 | - ⚡ **Reactive**: Seamless integration with Svelte's reactivity system
11 | - 🔄 **Auto-sync**: Automatically syncs state across components
12 | - 📦 **Simple API**: Just one function to manage all your state needs
13 |
14 | ## Installation
15 |
16 | ```bash
17 | npm install @friendofsvelte/state
18 | ```
19 |
20 | ## Quick Start
21 |
22 | 1. Define your state using `PersistentState`:
23 |
24 | ```typescript
25 | // new.svelte.ts / js
26 | import { PersistentState } from '@friendofsvelte/state';
27 |
28 | export const box = new PersistentState('box', {
29 | color: '#ff3e00',
30 | dimensions: [100, 100]
31 | }, 'sessionStorage');
32 | ```
33 |
34 | 2. Use in your components:
35 |
36 | ```svelte
37 |
52 |
53 |
57 | {box.current.color}
58 |
59 |
60 |
63 | ```
64 |
65 | ## API Reference
66 |
67 | ### `PersistentState(key: string, initial?: T, storageType: StorageType = 'localStorage')`
68 |
69 | Creates or retrieves a persistent state container.
70 |
71 | Parameters:
72 | - `key`: Unique identifier for the state container
73 | - `initial`: (Optional) Initial state value
74 | - `storageType`: (Optional) Storage type - 'localStorage' or 'sessionStorage' (default: 'localStorage')
75 |
76 | Returns:
77 | - A reactive state object of type `T`
78 |
79 | > Based on, Rich-Harris' [local-storage-test](https://github.com/Rich-Harris/local-storage-test/blob/main/src/lib/storage.svelte.ts)
80 |
81 |
82 | ## Examples
83 |
84 | ### Basic Usage
85 |
86 | ```svelte
87 |
101 |
102 |
103 | {box.current.color}
104 |
105 |
106 |
109 | ```
110 |
111 | ## Contributing
112 |
113 | Contributions are welcome! Please feel free to submit a [Pull Request](https://github.com/friendofsvelte/state/pulls).
114 |
115 | ## License
116 |
117 | MIT License - see LICENSE file for details
118 |
--------------------------------------------------------------------------------