-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat: $derived.by
get current value as first argument
#13250
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
Conversation
🦋 Changeset detectedLatest commit: 090f06a 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 |
On 2), if it instead initializes to |
I am of the same opinion, a simple
|
Some thoughts on why we shouldn't do this: #13249 (comment) |
I'd say having the previous value in For destructuring purposes I agree with @Rich-Harris. That does feel like an anti-pattern and should be discouraged (there is Also, accessing the previous value is trivial in Svelte 4 with just self-reference. And it returns $: double = (console.log(double), count * 2); Migrating the same code to Svelte 5 gives:
Which is bad for backwards compatibility. |
When writing the comment above, I've noticed that auto-migration generates invalid code. When trying to process $: double = (console.log(double), count * 2); it outputs let double = ($derived(console.log(double), count * 2); which is obviously wrong
REPL. We might need an issue for that (if there isn't already one). |
While I agree that it could be useful if you read Rich comment is pretty trivial to do already by storing the value in a non state variable.
Yeah I think we need one, if you can open one that would be great |
We can, but it feels like a hack... Having a function parameter is much more elegant and intuitive. It also nicely encapsulates the derivation logic without having to create additional variables in a scope where other logic might live in. Also, if I understand that correctly, there isn't any memory/performance overhead since Svelte internal store this value anyway.
Opened an issue #13278. |
Here are some cases where it might be useful:
In both cases effect is needlessly complicated and doesn't communicate developer's intent well. I would argue that these are pretty common types of problems in UI development (I personally encountered variations of these cases quite many times) |
Quoting Rich here
Those might be valid cases but there might be cases where you want all the previous values too...what to do then? Should svelte pass all the values? Considering the TS problem and the fact that you can easily do that yourself without an effect and with minimal code that also gives you flexibility of storing every previous value I lean towards we shouldn't rush this and add it only if we find that not doing it is way worse than doing it. |
I think this is irrelevant, since we already have the previous value. So, we can let a user decide what to do with it or whether to use it at all. I can see why adding anything beyond that (e.g. previous-but-one) wouldn't be feasible, because it must be implemented separately. But this is essentially a free feature that wouldn't even complicate the API (if you omit the parameter it's like it's never been there). Also from @Rich-Harris comment:
By the way, I might be not seeing the full picture here. Has there been any discussion around forking already? Would adding this API complicate its implementation? In that case, I can see why it might not be free. Though, as it stands right now, I see no reason not to add it. |
The point is that there's no such thing as a free feature. Once you have this API you also have to leave with the consequences of it and the forking example is a premier candidate of this concept. Granted that I don't have personally idea of how this feature will affect forking if Rich mentioned it is likely that will have an effect. And that could be true also for other features that one might want to introduce (maybe we realize that we want something passed to This is not to say we shouldn't add any feature for the fear of stopping something in the future but since is this case is very easy to have an alternative it might be worth. |
Hmm, I see... So, is it settled that we won't have it or is it still open to discussion? Do we need more opinions here? |
still open to discussion, just that it won't happen right now |
I've been thinking about this more, and kind of like the idea of making
Plus, we can type the Here's an example of how the previous value can be really useful but this hacky way will cause countless issues in the future. It would essentially allow us to create However, one common complaint with this pattern is "how do I know the thing I really care about is update?". i.e. if you had: let local = $derived.by((prev) => {
if (prev !== null && draft) {
return prev;
}
const local_state = $state({ count });
prev = local_state;
return local_state;
}, null); Then when Further thoughtsThat has got me thinking about change detection mechanics. Maybe we make it explicit by having a third argument that is for fine-grain change detection: let local = $derived.by((prev, [ count_changed ]) => {
if (prev !== null && (draft || !count_changed)) {
return prev;
}
const local_state = $state({ count });
prev = local_state;
return local_state;
}, null, [ count ]); Now we know when specific things change (internally we call this dirty) and in this case we know when Food for thought, but I think this inherently solves many of the issues |
@trueadm, do you suggest to use a compiler to differentiate between an explicit undefined Other than that, the second argument approach is fine (better than nothing at all). Though I'd prefer
Couldn't that be achieved with
Solving |
For comparison, here's a userland higher order function that provides the previous value. Something similar could be implemented for arbitrary history, different handling of initial values, cache busting behaviors, etc. Syntax isn't as nice as a builtin of course - REPL const doubled = $derived.by(with_previous((prev) => {
// would use `prev` here, the example just logs and outputs to the DOM
return count * 2;
}, null));
function with_previous(cb, initial) {
let prev = initial;
return () => (prev = cb(prev));
}; You could also call onDestroy in the helper, and you could avoid boilerplate with a hardcoded destroy interface like |
It can be done at runtime.
I really dislike treating
To be clear here, this isn't a dependency array. It's about as far from a dependency array as possible. It's more of a way of letting the derived know of change detection for specific things. |
@ryanatkn this is brilliant, and cements my position that we don't need this in the framework, at least not any time soon. Your REPL shows a workaround for the fact that you can't set state from a derived, but you don't actually need it — you can use the previous value in the returned value. In the interests of making progress towards a release, I think we should close this PR. |
Svelte 5 rewrite
Closes #13249
This is an implementation but it needs discussion for sure for the following reasons:
argument.length
(namely the value returned from$.prop
) it's kinda risky because, like in this case, if we decide in the future to pass something in to the function that uses them removing the thunk will introduce a bug...this time luckily it was caught by two tests...but that's just two tests who knows in the future. The alternative should be a big refactor to always usecreate_derived
and check in there if the binding is a prop or a reactive import. I went this route because it feels a micro optimisation relative to the difficult to debug bugs we could be getting.$derived.by
destructor support (Svelte 5) #13249 (comment) this implementation passnull
as the first value...but this means that if the value is actuallynull
you'd have no way of knowing if this the initialisation or the actual value. We could either pass a second argument to tell if it's the initialisation or pass a symbol instead ofnull
. Both are not very appealing to me.could be simplified with just
because
.v
is only defined for deriveds but i wanted to avoid the situation where in the future we change that and this introduces a bug (or if we readarguments.length
somewhere else).5. typescript is also not super-good here...if you actually get the value you need to type it or it will be
unknown
(i wasn't able to find a way to make TS infer that from the return value) and you also need to type it asYourType | null
or it will complain.6. Do we actually want this solution or do we prefer to go for something else?
No test for the moment but i'll add one when we reach a consensus.
Please note that the Svelte codebase is currently being rewritten for Svelte 5. Changes should target Svelte 5, which lives on the default branch (
main
).If your PR concerns Svelte 4 (including updates to svelte.dev.docs), please ensure the base branch is
svelte-4
and notmain
.Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.Tests and linting
pnpm test
and lint the project withpnpm lint