Open elron opened 5 days ago
Sorry, I mean do you have plan to migrate to $state and class declarations instead of writables?
TL;DR
It's possible, but not as neatly as with store. (worst DX than with store)
Runes are very complicated to customize.
Runes are compiler concept that are replaced at compilation time.
They don't have any interface nor behavior contract nor type, so it's impossible to do Liskov substitution or high order function (wrapper) on them.
The only possible way is with class, but that exclude all primitive value like: let name = $state('John')
and it won't be a wrapper which is a bit limiting
And as runes are only available a compilation time, we can't have dynamic class (see below)
The best I can think of is this:
<script>
const name = persistState('John', createLocalStorage(), 'name')
</script>
<h1>Hello, {name.value}!</h1>
(.value
is mandatory, no other solution)
If you really want it, even with this limitation, it can be added
Limitation with dynamic class
As runes are only available at compilation time and $state
only available for property declaration and variable declaration in component root, I can't offer a solution like this
<script>
const storage = createLocalStorage()
class Person extends PersistableState({ name: storage, age: storage}) {
// ... your custom logic
}
const john = new Person({name: 'John', age: 33})
// On first launch, john will be `{name: 'John', age: 33}`
john.age = 34
// On later use, john will be loaded with age=34
const initial = $derived(john.name.substring(0, 1))
</script>
<h1>Hello, {john.name}!</h1>
<span>{initial}</span>
Which could be quite nice DX, but Svelte compiler don't allow it
It could exist a solution with a similar DX as store, but it will need to be a Vite plugin in order to "on the fly" replace code at compilation. And this is far beyond the purpose of this project
Also, it will not be (and will never be) a migration, but an addition, Store a still relevant in many cases
Thanks for thinking it through thoroughly. What is "DX" that you mentioned a couple of times?
Anyway, I wanted to experiment with svelte-5 and classes, and I figured out a clear and short solution for my app.
No stores, only runes & classes.
I thought I'd show you what I did for inspiration:
store.svelte.ts
file (handles the states):
import { Persist } from "$lib/utils/Persist.svelte";
export const software = new Persist(
"" as "fontcreator" | "glyphsapp" | "",
"software"
);
Persist.svelte.ts
file (handles all the logic, but only with local-storage at the moment - can be expanded for more use cases):
import { browser } from "$app/environment";
export class Persist<T> {
#value: T = $state("" as T);
#name: string | undefined = $state();
constructor(defaultValue: T, name: string) {
if (!browser) return;
this.#name = name;
const localValue = localStorage.getItem(name);
if (localValue) {
this.value = JSON.parse(localValue);
} else {
this.value = defaultValue;
this.set(defaultValue);
}
}
set(value: T) {
if (this.#name) {
localStorage.setItem(this.#name, JSON.stringify(value));
} else {
alert(`this.#name is ${this.#name}`);
}
}
set value(newValue: T) {
if (this.#name && newValue) {
this.set(newValue);
}
this.#value = newValue;
}
get value(): T {
return this.#value;
}
}
And to use it in the app, simply:
<script>
import { software } from "$lib/layouts/store.svelte";
</script>
Current software is {software.value}
What do you think of this?
What is "DX" that you mentioned a couple of times?
Developer eXperience.
It's how easy and natural this lib usage is for others developer.
The more you have to do, configure and/or change your code to make the lib works, the lower is the DX
Your solution is not far from my own test. What I got is this:
export interface PersistentState<T> {
value: T;
delete(): void;
}
export function persistState<T>(initial: T, storage: StorageInterface, serialization: Serialization<T>, key: string): PersistentState<T> {
const rawValue = storage.getValue(key)
const initialValue = rawValue ? serialization.deserialize(rawValue) : null
let state = $state(initial);
if (null !== initialValue) {
state = initialValue
}
if ('addListener' in storage) {
;(storage as SelfUpdateStorageInterface).addListener(key, (newValue: string) => {
state = serialization.deserialize(newValue)
})
}
$effect.root(() => {
$effect(() => storage.setValue(key, serialization.serialize(state)))
})
return {
get value() {
return state
},
set value(newValue) {
state = newValue
},
delete() {
storage.deleteValue(key)
},
}
}
(You also have a sneak peek of the version 3 of the lib :smile: )
Your version can probably be simplified into this:
import { browser } from "$app/environment";
export class Persist<T> {
// The compiler will handle the getter / setter for you
value: T = $state("" as T);
constructor(defaultValue: T, private readonly name: string) {
if (!browser) return;
// Use the $effect rune to update the localStorage
// $effect.root allow us to declare effect outside a component
$effect.root(() => {
$effect(() => this.set(this.value))
})
const localValue = localStorage.getItem(this.name);
if (localValue) {
this.value = JSON.parse(localValue);
} else {
this.value = defaultValue;
}
}
set(value: T) {
// No need to add control on `name`, it's a require parameter from the constructor, it always exists
localStorage.setItem(this.name, JSON.stringify(value));
}
}
The more I think about it, the more I feel that a Vite plugin (or maybe a Rollup plugin) will be better.
We could achieve something like this:
let title = $persist('Unsaved document', 'document.title')
Then the plugin kick in and give this to the compiler
import { saveToStorage, loadFromStorage } from "..."
let title = $state(loadFromStorage(document.title) ?? 'Unsaved document')
$effect.root(() => {
$effect(() => saveToStorage(title, 'document.title'))
})
So the variable can be used without the additional .value
, it will be almost transparent as the diff between a $state
and $persist
is minimal:
- let title = $state('Unsaved document')
+ let title = $persist('Unsaved document', 'document.title')
Thanks!! Good to see your work there.
This last example sounds good! How would you implement TS type safety?
+ let title = $persist('Unsaved document', 'document.title')
How would you implement TS type safety?
I didn't test this solution at all. But $state
type declaration is just this: https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/ambient.d.ts#L23
So it would be something like:
declare function $persist<T>(initial: T, key: string): T;
declare function $persist<T>(key: string): T | undefined;
Sounds good!
This library is already supporting Svelte 5.
See #59 and version
2.4.2
I use it on some SvelteKit + Svelte5 projects without any issues