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

feat: add $state.opaque rune #14639

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft

feat: add $state.opaque rune #14639

wants to merge 14 commits into from

Conversation

trueadm
Copy link
Contributor

@trueadm trueadm commented Dec 9, 2024

This PR adds the $state.opaque rune. This is a special kind of rune designed to solve problems with handling and managing state from external sources/libraries.

Specifically, for cases where Svelte is not involved in understanding that of the reactivity of the thing you're passing in – thus being opaque to Svelte. In order to let Svelte 5 know that something has changed, an invalidate function is provided so you can manually control letting Svelte know it should invalidate any reactive dependencies on this piece of state:

<script>
  let [count, invalidate_count] = $state.opaque(0);
</script>

<button onclick={() => count++ }>+</button>

<button onclick={() => invalidate_count() }>invalidate</button>

<div>{count}</div>

This means that reassignments and mutations to opaque state will not trigger reactive updates. You always need to invoke the invalidate function. Furthermore, this means that you will likely need to adopt the recent function bindings feature if you intend to use opaque state with bind:something.

Closes #14520.

Copy link

changeset-bot bot commented Dec 9, 2024

🦋 Changeset detected

Latest commit: 21b0865

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Rich-Harris
Copy link
Member

preview: https://svelte-dev-git-preview-svelte-14639-svelte.vercel.app/

this is an automated message

@trueadm trueadm changed the title feat: add $state.opqaue rune feat: add $state.opaque rune Dec 9, 2024
Copy link
Contributor

github-actions bot commented Dec 9, 2024

Playground

pnpm add https://pkg.pr.new/svelte@14639

@trueadm
Copy link
Contributor Author

trueadm commented Dec 9, 2024

TODO: when we land $inspect.trace we need to add a call-site to this state so it's name is traced too.

@Rich-Harris
Copy link
Member

Given the syntactical requirements, I assume there's no way to make this work with classes?

@trueadm
Copy link
Contributor Author

trueadm commented Dec 11, 2024

Given the syntactical requirements, I assume there's no way to make this work with classes?

Nope. I think that’s fine though

@Ocean-OS
Copy link
Contributor

Ocean-OS commented Dec 12, 2024

Given the syntactical requirements, I assume there's no way to make this work with classes?

Nope. I think that’s fine though

Couldn't something like this be done? (apologies for the terrible spacing)

class Thing{
    constructor() {
        ([this.stuff, this.invalidate] = $state.opaque(stuff)); 
    }
	}

Couldn't the private property for the $state object be added in compile time?
There could be a separate internal opaque_state function that specifically works with classes.

@dummdidumm
Copy link
Member

@trueadm
Copy link
Contributor Author

trueadm commented Dec 12, 2024

I don't think we should optimise for the usage of $state.opaque on classes, as long as there are ways to make it work (even if fugly) then that's fine IMO.

@Leonidaz
Copy link

Leonidaz commented Dec 14, 2024

What if $state.opaque is allowed to work with both a destructured assignment and also with a direct assignment, e.g.

// direct assignment that compiles into an object usage:
let count = $state.opaque(0);

// changing state would be:
// the prop name could  be named differently
// but that's what's being used in Svelte in other places, e.g. `value`
count.current++;

// update, 
// the default name could by the variable name with pre-pended `update`, e.g. `updateCount`
count.update();

// or object destructure with optional renaming, this will generate the same code as it does now in this pr

let { current: count, update: updateCount } = $state.opaque(0);

count++;
updateCount();

class for the non-destructured declaration would be: (otherwise, people can use a constructor with destructured)

// from:

class Thing {
 count = $state.opaque(0); // if count is declared as private `#count`, nothing is generated as per usual.
}

// to:

class Thing {
  #_count = $state.opaque(0);
  
  get count() {
    return this.#_count.current;
  }

  set count(v) {
    this.#_count.current = v;
  }
  

// if `update` already exists could name it `updateCount`
// a bit more complicated but `$state.opaque()` can take a second parameter `options` 
// e.g. $state.opaque(myState, {updateName: 'updateCount', propName: 'current'})
  update() {
    this.#_count.update();
  }
}

Typescript will show the prop names and it would be documented.

What are the benefits vs the current approach?

When using a non-destructured variant, it will allow people to keep the update method, together with the state.

As a side-effect, if kept non-destructured, this would keep the reactivity intact for crossing function boundaries. See $state.unwrap() below, if svelte can make this a pattern.

It's possible to generate a class definition.

What are the cons vs the current array destructure approach

If destructuring and renaming variables, it's more verbose.

Later, maybe also introduce a rune $state.unwrap() that could take the object return of $state.opaque(0) and destructure it. Also, could work with other objects created by other state runes. The generated code would still refer to the original state though to have one synchronized state.

// somewhere
let count = $state.opaque(0);

// somewhere else
let { current: count, update: updateCount } = $state.unwrap(count);

// but in reality the generated code would still be referring to and updating `count.current`
// so only one state exist and there is no de-synchronization.

other states:

// somewhere
let something = $state({ count: 0 })

// somewhere else
let { count } = $state.unwrap(something);

// the generated code would still be referring to and updating `something.count`

@trueadm
Copy link
Contributor Author

trueadm commented Dec 14, 2024

What if $state.opaque is allowed to work with both a destructured assignment and also with a direct assignment, e.g.

That's far too many new APIs for something we don't want to promote as a core pattern.

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

Successfully merging this pull request may close these issues.

add a way to force update a state variable
5 participants