├── .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 | --------------------------------------------------------------------------------