Skip to content
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

New runes $bind() / $read() for easier access to props/state #9951

Closed
adiguba opened this issue Dec 18, 2023 · 7 comments
Closed

New runes $bind() / $read() for easier access to props/state #9951

adiguba opened this issue Dec 18, 2023 · 7 comments
Labels

Comments

@adiguba
Copy link
Contributor

adiguba commented Dec 18, 2023

Describe the problem

$state()/$props() are very practical within *.svelte component files, but it may be difficult to use in others files (.js/.ts or even .svelte.js/.ts), because we have no way to pass the state himself, but only the value.

For example take this code :

	let count = $state(0);
	// ...
	fn(count);

I will produce a warning :

State referenced in its own scope will never update. Did you mean to reference it inside a closure?

The solution is to use closures, so

	// for read-only access
	fn( () => count );
	
        // for read/write access we need two closures :
	fn( () => count,
	     (v) => count = v );

But it's a little more verbose and less readable.

Describe the proposed solution

A solution could be to define runes to automate this.

For example $read() for a read-only access, and $bind() for a read/write access.

  • $read() will simply return a closure
  • $bind() will return the same closure, containing an additional attribute for update.

The return type of these runes will match this (expressed in TypeScript) :

// $read() return 
type ReadState<V> = () => V;

// $bind() return 
type BindState<V> = ReadState<V> & { 
    set: (v: V) => void
};

So :

fn($read(count));
// will be equivalent to : 
fn( () => count );

fn($bind(count));
// will be equivalent to : 
const bind_count = () => count;
bind_count.set = (v) => count = v;
fn(bind_count);

And inside the function we have a closure that we can use to access the value of the element :

function fn(val) {

    // read
    console.log("val = " + val());

    // write
    val.set( 0 );
    
    // fine-grained reactivity :
    $effect( () => {
          console.log("effect on " + val());
    });
}

Example with an action that update state : REPL

Alternatives considered

using separate closure like now

Importance

nice to have

@7nik
Copy link

7nik commented Dec 18, 2023

Isn't wrapping the state into an object easier? Also thus, the state is extendable to additional data without breaking code.

@brunnerh
Copy link
Member

brunnerh commented Dec 18, 2023

Also thought about $box and maybe $box.readable to create a { value } object with and without setter as a (not so great) alternative.

Related:

@adiguba
Copy link
Contributor Author

adiguba commented Dec 18, 2023

Isn't wrapping the state into an object easier? Also thus, the state is extendable to additional data without breaking code.

It's more verbose and less pratical, and will impact the template :

<script>
	let ref = $state({count:0});
        fn(ref);
</script>
{ref.count}

VS

<script>
	let count= $state(0);
        fn($bind(count));
</script>
{count}

@brunnerh
Copy link
Member

brunnerh commented Dec 18, 2023

You don't necessarily have to wrap it from the start but can wrap it while passing it to the function.

@ptrxyz
Copy link

ptrxyz commented Jan 12, 2024

I find it pretty impractical to having to use yet another rune to wrap a rune just to pass it along tbh.
I can see the problem, but it's not a good solution to change one boilerplate to another.

@7nik
Copy link

7nik commented Jan 12, 2024

What about bypassing a pointer to a state?
E.g.

function createCounter() {
  let count = $state(0);
  return $pack(count);
}

let count = $state.from(createCounter());

will compile to

const privateGlobalMap = new WeakMap();

function createCounter() {
  let rawSignal = $state();
  let key = Symbol(); // or just {}
  privateGlobalMap.set(key, rawSygnal);
  return key;
}

let count = privateGlobalMap.get(createCounter());

Thus, the signal objects aren't exposed but can still be directly passed.

However, there is the question of what should happen when you $pack a non-signal or do $state.from for not a packed signal.

@Rich-Harris
Copy link
Member

Closing for the reasons given in #9237 (comment)

@Rich-Harris Rich-Harris closed this as not planned Won't fix, can't repro, duplicate, stale Apr 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants