Skip to content

$state.lazy #15620

Closed
Closed
@WillsterJohnson

Description

@WillsterJohnson

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions