-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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 onchange
option to $state
#15069
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 714c042 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
preview: https://svelte-dev-git-preview-svelte-15069-svelte.vercel.app/ this is an automated message |
|
Would there also be an |
Yes (uh I need to update types for that too) |
I like this, but also I'm thinking that there are more cases where you get a |
I think effect or derived with state is the good formula for that already...this aims to solve the problem of getting deep updates for a stateful variable that you own. |
This would have helped me several times! Figuring out how to watch deep updates was very unintuitive to me. |
Recapping discussion elsewhere — I think we made a mistake with the implementation of let obj = {};
let a = $state(obj);
let b = $state(obj);
console.log(a === b); // false, which is good
let c = $state(b);
console.log(b === c); // true, which is bad There's no real point in having svelte/packages/svelte/src/internal/client/proxy.js Lines 32 to 35 in de94159
It would have been better if existing proxies passed directly to Most of the time this doesn't really matter, because people generally don't use state like that. But we do need to have some answer to the question 'what happens here?' more satisfying than 'we just ignore the options passed to let obj = { count: 0 };
let a = $state(obj, {
onchange() {
console.log('a changed');
}
});
let b = $state(obj, {
onchange() {
console.log('b changed');
}
});
let c = $state(b, {
onchange() {
console.log('c changed');
}
}); I think one reasonably sensible solution would be to throw an error in the |
I think it'd probably be best if |
Actually, I take it back — in thinking about why we might have made it work that way in the first place, this case occurs to me: let items = $state([...]);
let selected = $state(items[0]);
function reset_selected() {
selected.count = 0;
} That doesn't work if we snapshot at every declaration site (and it certainly wouldn't make sense to snapshot on declaration but not on reassignment). So I guess automatic snapshotting and erroring are both off the table — we need to keep the existing behaviour for let c = $state(b, {
onchange() {...}
}); let c = $state({}, {
onchange() {...}
});
c = b; |
I didn't think of that, actually. But couldn't that be fixed with $state.link? That way, we wouldn't have to worry about let items = $state([...]);
let selected = $state.link(items[0]);
function reset_selected() {
selected.count = 0;
} |
Not sure how I feel about this. Shouldn't this just be some kind of effect, that watches deeply?
|
… const
No. Avoiding effects is the whole point of this. Deep-reading is easy, but often when you want to respond to state changes, the state was created outside an effect (think e.g. an abstraction around Opened #15073 to address an issue with array mutations causing |
* only call onchange callbacks once per array mutation * fix * fix
packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js
Outdated
Show resolved
Hide resolved
packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
Outdated
Show resolved
Hide resolved
packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js
Outdated
Show resolved
Hide resolved
was in the branch to merge |
Great, I'll see if I can split the tests today |
@Rich-Harris splitted the tests up |
packages/svelte/tests/runtime-runes/samples/state-onchange-reassign-proxy/main.svelte
Outdated
Show resolved
Hide resolved
…ssign-proxy/main.svelte
…vent with the subsequent assertion
What's the value of the |
I think the issue here is, this makes it seem like |
Yeah watch without I mean we could make it work but I just don't understand what the advantage would be (it also looks very ugly) |
What this PR does it's different from using effect since it's invoking the callback synchronously... it's also doing it deeply without the need to create a source for the whole object. |
What's not clear about an object with |
Nope, it's just the compiler that is unwrapping the object to pass just the onchange to the function so the API is still the same |
Closes #15032
This adds an
onchange
option to the$state
runes as a second argument. This callback will be invoked whenever that piece of state changes (deeply) and could help when building utilities where you control the state but want to react deeply without writing an effect or a recursive proxy.Edit: @brunnerh proposed a different transformation which is imho more elegant...gonna implement that tomorrow...instead of creating a state and a proxy externally we can create a new function
assignable_proxy
that we can use in the cases where we would do$.state($.proxy())
do that we can not declare the extraconst
orprivate_id
. We could also pass a parameter toset
to specify if we should proxify or not and that could solve the issue ofget_options
.Edit edit: I've implemented the first of the above suggestions...gonna see what is doable with
set
later...much nicer.Edit edit edit: I've also implemented how to move the proxying logic inside
set
...there's one issue which i'm not sure if we care about: if you reassign state in the constructor of a class we don't call set do it's not invoking the onchange...we can easily fix this with another utility function that does both things but i wonder if we should.A couple of notes on the implementation. When initialising a proxy that is also reassigned we need to pass the options twice, once to the source and once to the proxy...i did not found a more elegant way so for the moment thisis compiled toif the proxy is reassigned.Then when the proxy is reassigned i need to pass the same options back to the newly created proxy. To do so i exported a new function from the internals
get_options
so that when it's reassigned the proxy reassignment looks like thisthe same is true for classes...there however i've used an extra private identifier to store the optionsis compiled tothere's still one thing missing: figure out how to get a single update for updates to arrays (currently if you push to an array you would get two updates, one for the length and one for the element itself.
Also also currently doing something like this
and updating bar will only trigger the update for
foo
.Finally the
onchange
function is untracked since it will be invoked synchronously (this will prevent updating from an effect adding an involountary dependency.Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.packages/svelte/src
, add a changeset (npx changeset
).Tests and linting
pnpm test
and lint the project withpnpm lint