Skip to content

$state.lazy #15620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
WillsterJohnson opened this issue Mar 26, 2025 · 3 comments
Closed

$state.lazy #15620

WillsterJohnson opened this issue Mar 26, 2025 · 3 comments

Comments

@WillsterJohnson
Copy link

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

@MotionlessTrain
Copy link
Contributor

$derived is writable as of svelte 5.25, so you can actually use that now (provided the initialisation function doesn’t refer a state or another derived, otherwise it could rerun when those update)

@Thiagolino8
Copy link

As @MotionlessTrain said, $derived is no longer read only
Despite this, a setter is not created automatically, I opened an issue for this

@WillsterJohnson
Copy link
Author

To think simply running pnpm i might've saved me the hour I spent trying to work around this.

Closing this in favor of #15621

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants