The `data` prop and subsequent `$derived` not mutable by default
Describe the bug
Now that $derived has been made writable. This is intended to make optimistic UI updates possible without having to use the $state in $derived.by dance. However, it still doesn't entirely solve the problem because data is a $state.raw, which means mutations to data or its deriveds won't update the UI. To make it work like Svelte 4 did, the trick is still needed, which is kinda ugly and not beginner friendly.
I believe making data a full $state instead of $state.raw would solve this problem. Any reasons not to do so?
Reproduction
https://svelte.dev/playground/a69b38c2e041412989ca3edb509a0138?version=5.25.2
Logs
System Info
Svelte 5.25.2
SvelteKit 2.20.2
Severity
annoyance
Additional Information
No response
IMO state would be a good default but switching to raw when needed should be as simple as possible, although this may mean making data less magical and more explicit.
I think this is intentional (though Rich might hop in here and tell me I'm wrong). What you can do (and what we prefer) is to make it explicit that you intend to optimistically update something:
const { data } = $props();
const likesCheeseburgers = $derived(data.likesCheeseburgers);
likesCheeseburgers = true;
Right, but that only works if you reassign likesCheeseburgers as a whole, which makes sense if it's a primitive like boolean.
If it's an object and we want to mutate the object instead of reassigning it, it doesn't update the UI.
Right, but that only works if you reassign
likesCheeseburgersas a whole, which makes sense if it's a primitive likeboolean. If it's an object and we want to mutate the object instead of reassigning it, it doesn't update the UI.
Exactly! The examples given by the $derived / data example are just too simplified. Usually data contains a complex structure, like for example some business data, e.g., a customer that you return as data and that you want to edit as part of the page and then sent back to the server (either directly or using server actions).
Most of the time - and the way the APIs work push you in that direction - it's better to not mutate, instead reassign the data. In other words, instead of data.foo = 'bar' you do data = { ...data, foo: 'bar' }. That way the update is truly optimistic since it's local to your component, and doesn't have unintended consequences in other parts of your app.
Most of the time - and the way the APIs work push you in that direction - it's better to not mutate, instead reassign the data. In other words, instead of
data.foo = 'bar'you dodata = { ...data, foo: 'bar' }. That way the update is truly optimistic since it's local to your component, and doesn't have unintended consequences in other parts of your app.
Then what's the point of having the rune system in the first place then?
Universal reactivity, easier to read code, more predictable behavior, being able to deprecate (and at some point remove) APIs like $$slots/$$props/$$restProps, and much more
Most of the time - and the way the APIs work push you in that direction - it's better to not mutate, instead reassign the data. In other words, instead of
data.foo = 'bar'you dodata = { ...data, foo: 'bar' }. That way the update is truly optimistic since it's local to your component, and doesn't have unintended consequences in other parts of your app.
It's easy to say when foo is only 'bar'. It's really unhandy when data contains deeply nested objects and arrays. I don't understand the hypothetical consequences of doing this:
data.deeply.nested.object.property= 'new value'
vs this:
data = {...data, deeply: {...data.deeply, nested: {...data.deeply.nested, object: {...data.deeply.nested.object, property: 'new value'}}}};
It wasn't like this in Svelte 4.
I feel like this is a purist ideology similar to '$derived is classically considered read-only' that should be reconsidered.
I also think this might be worth considering but if this is enabled by adding a state proxy to data (and maybe form), this would also be a breaking change...
I can't think of anything this change would 'break', but perhaps I'm ignorant. I think this is more an enhancement rather than a breaking change. Like moving from read-only $derived to writable is also technically a breaking change. If the change does break something existing and/or have unintended consequence, at least make it easy to opt-in and keep the current default.
It currently doesn't affect data in other components.
I presume wrapping data in a state proxy changes that.
If you're talking about page.data then right. However currently nobody is actually doing page.data mutation, at least without a hacky workaround.
If data is a fully proxified $state and you want to make changes local to the current component only, it's easy to do:
let localData = $derived($state.snapshot(data));
If a dev wants to mutate data (and the global page.data) directly, let them deal with the possible consequences. Make it explicit with $bindable and warn with invalid binding mutation, which is in line with how Svelte works in other components
The change to $derived was not breaking because prior to the change, reassignment was prevented by the compiler, code doing it could not exist.
Adding a proxy is a breaking change because certain APIs just do not work with them (e.g. structuredClone), this code would then throw errors.
So it seems the least invasive solution is proxifying data in a $derived.
It would've been nice if this syntax was allowed as an alternative to the current $state in $derived.by trick:
let localData= $derived($state(data));
It's simple and straight-forward for anyone new to Svelte(Kit) to understand
We (@shivan-eyespace) have this problem. Our workaround is to use $state and $effect and turn off the preferred $derived error.
https://svelte.dev/playground/083a578acc5b4e34be3f19ee86ab2471?version=5.33.16
Must give @henrykrinkle01's example a go.
@shivan-s you can actually just use $derived.by in your case, which avoids the effect: https://svelte.dev/playground/a91a0b02e26849c4b733c92d61773ef3?version=5.33.16
@elliott-with-the-longest-name-on-github Genius! Thank you.
@shivan-s you can actually just use
$derived.byin your case, which avoids the effect: https://svelte.dev/playground/a91a0b02e26849c4b733c92d61773ef3?version=5.33.16
$derived($state(...)) could benefit from a more specific error message, as:
$state(...) can only be used as a variable declaration initializer [...]
although technically right, makes it sound like what you're trying to do is a dead end. Getting in this situation you need to overcome this message, remember that $derived.by() exists, and realize that variables can be declared inside it, which is obvious when you think about it but is still is some sort of mental detective work.
Also, just out of curiosity and due respect, would you classify this issue as svelte related or, as it is, kit related?
It's both. There've been related issues opened in both branches. There are cases where this problem can arise in Svelte alone, but mostly it's related to the data prop, which belongs to Kit.
I'm having the same problem and solved it partly with elliots solution. Despite that, I still have weird behavior when I want to update nested objects in my repl
let flatSlugDetails = $derived.by(() => {
let _ = $state( // using state here to make the whole object deeply reactive
data.userFlats.filter((obj) => obj.flatid === page.params.flat)[0]
);
return _;
});
The state of rating per user updates when I update my rating over the object->list->rating directly, but not when I go over the abstracted $derived way. Why is that and is there a way to make it still work over the $derived way for the average calculation? https://svelte.dev/playground/387bd3c8cd8343e3b8b5b6ac25758017?version=5.45.3
It works as intended
let currentUserReview = $derived(applicantDetailsData.evaluation[data.session.user.id]);
When you write to currentUserReview, you don't update applicantDetailsData.evaluation[data.session.user.id] directly but just a cloned version of it.