Description
Describe the problem
To give a class a lazy property in JavaScript, we can do the following (quite verbose);
class LazyFoo {
#foo
#fooInit() {/* some expensive computation that may or may not be needed */}
get foo() {
return (this.#foo ??= this.#fooInit())
}
set foo(value) {
this.#foo = value
}
}
One mistake can cost a lot of debugging time if you're not careful, which is why an @lazy
decorator can be so valuable.
A lazy reactive property requires all the same boilerplate;
class ReactiveLazyFoo {
#fooImpl = $state() // we want `foo` to be writable, so we can't simply use `$derived.by`
#fooInit() {/* some expensive computation that may or may not be needed */}
get foo() {
return (this.#fooImpl ??= this.#fooInit())
}
set foo(value) {
this.#fooImpl = value
}
}
Describe the proposed solution
please note that I'm not well versed in Svelte's internals, so the compiler output samples below are just to get the general idea across, not to give concrete suggestions
We could have $state.lazy
;
class ReactiveLazyFoo {
foo = $state.lazy(() => /* expensive */)
}
Which transpiles to;
import * as $ from 'svelte/internal/client';
class ReactiveWritableLazyFoo {
#foo = $.state();
#foo__initializer = () => /* expensive */
get foo() {
// if the init fn exists, call it, set foo, then clear the initializer (saves having a boolean flag, and after one call the function is no longer needed)
this.#foo__initializer && (this.#foo = this.#foo__initializer())
return $.get(this.#foo);
}
set foo(value) {
delete this.#foo__initializer
$.set(this.#foo, value, true);
}
}
This wouldn't add to Svelte's internal runtime, instead being a compiler change to treat $state.lazy
the same as $state
, but with the additional field and the additional statements in the getter & setter.
In components (or just variables in general), the output would have to include notably more additional code than in .svelte.[js|ts]
, as there now needs to be some operation around the initializer on every $.get
and $.set
. It might be better to emulate the class behavior; create a getFoo
and setFoo
function which call $.get
and $.set
respectively, along with the operation on the initializer.
It may even be better to disallow $state.lazy
outside of class fields entirely given the amount of additional work the compiler would have to do. That, and reactive values inside of components benefiting from laziness is much rarer than class fields.
In cases where it is desirable, a static class can be created, which is relatively inexpensive. Or, if it turns out to be cheaper than the alternatives, the compiler could inject a static class in place of some let foo = $state.lazy(...)
, though that doesn't seem ideal.
Importance
would make my life easier