Skip to content

Svelte 5: Introduce $derived.with rune #9968

@Not-Jayden

Description

@Not-Jayden
Contributor

Describe the problem

This is a bit of a rehashing of #9250 prompted by seeing more of this discussion coming up in the Discord

TL:DR there's still animosity around people's preferences regarding the design choice for $derived accepting an expression instead of a callback function, and how it becomes slightly less ideal for the scenarios where you want to encapsulate more complex logic that wraps onto multiple lines.

Describe the proposed solution

Introduce a $derived.with rune, which accepts a callback for determining the derived value instead of an expression.

e.g.

const total = $derived.with(() => {
  const discountAmount = cartTotal * promoDiscount;
  const taxedAmount = (cartTotal - discountAmount) * taxRate;

  return cartTotal - discountAmount + taxedAmount;
});

Alternatives considered

  • Keep using IIFE's or defining the logic in functions outside of $derived, given it's really not that big of a deal
  • A separate $computed rune instead of not being nested under $derived
  • Alternative method names: call, compute, using, from

Importance

nice to have

Activity

kyle-n

kyle-n commented on Dec 20, 2023

@kyle-n

As the person who sparked the Discord discussion with my blog post, I'd like to second this proposal.

Having the option to use a callback to $derived.with() or $computed() means we avoid verbose alternatives like:

  • IIFEs (awkward syntax, easy to mistype)
  • Separate function that’s never reused anywhere (harder to follow reading, extra indirection, unnecessary hoop-jumping)

I appreciate Svelte's focus on writing less code and hope it will apply here too.

aradalvand

aradalvand commented on Dec 25, 2023

@aradalvand

Seconded, though I prefer the overload approach, adding an extra overload that accepts a function:

let foo = $derived(() => {
    // do calculations and return
});

While preserving the current signature as the other overload:

let bar = $derived(foo * 2);

But if overloads aren't feasible, $derived.with is fine as well, either way, this should be added IMO.

Not-Jayden

Not-Jayden commented on Dec 26, 2023

@Not-Jayden
ContributorAuthor

I worry overloading $derived to behave differently depending on whether it receives a function would probably cause too much confusion.

For example in this case:

function getTotal() {
  const discountAmount = cartTotal * promoDiscount;
  const taxedAmount = (cartTotal - discountAmount) * taxRate;

  return cartTotal - discountAmount + taxedAmount;
}

const total = $derived(getTotal);

The way $derived() works now it's easy to understand you're just going to get the reactive result of the expression passed into it. If it were to be overloaded, it's not immediately obvious to me whether total would be a function, or if it should get called and return the value reactively.

Also need to consider that it would be a breaking change to alter how $derived behaves directly, even though 5 isn't a GA release yet.

kyle-n

kyle-n commented on Dec 26, 2023

@kyle-n

I think overloading $derived to accept an expression or a callback would be ideal. Checking if something is a function is pretty negligible performance-wise. Allowing callbacks also provides the shortest, most developer-friendly syntax.

Svelte is all about writing less code. Let’s not have a separate function or an IIFE or $derived.with if we can skip it (though it’s an acceptable second option in my book).

TGlide

TGlide commented on Dec 26, 2023

@TGlide
Member

Overloading is not the best idea. What if I want to return a function from derived? Now I need to return a function from within a function.

aradalvand

aradalvand commented on Dec 27, 2023

@aradalvand

Okay, the reasons against it being an overload sound compelling. $derived.with seems to be the ideal approach then.

D-Marc1

D-Marc1 commented on Dec 29, 2023

@D-Marc1

Personally, I still think my original proposal of $derived() only allowing a function passed in is the most ideal. It's very confusing for someone coming into Svelte for the first time seeing this: const countDoubled = $derived(count * 2), as this is not allowed in vanilla JS.

The Svelte 5 literature constantly states that the reason for their changes are to reduce the learning curve, so why go against that when it comes to $derived()? I still don't get why the Svelte core team thinks that this non-standard syntax is worth it all to save four characters for a single line: const countDoubled = $derived(() => count * 2). I don't see what's so bad about this. Not to mention, the current $derived() syntax adds four extra characters for multiline, as you need to wrap it in an IIFEE, so it ends up being a wash, anyway.

Current $derived() multiline

const countDoubled = $derived((() => {
  return width * height
})())

Function argument proposal for $derived() multiline

const countDoubled = $derived(() => {
  return width * height
})
MrWaip

MrWaip commented on Jan 5, 2024

@MrWaip

This is definitely a necessary feature.

We have a ton of derived stores that compute complex conditions. Without that proposal we will get a problems while migration

TGlide

TGlide commented on Jan 5, 2024

@TGlide
Member

This is definitely a necessary feature.

We have a ton of derived stores that compute complex conditions. Without that proposal we will get a problems while migration

Why is it necessary? It's just a syntax change, you can do what's proposed with a different syntax (IIFEs).

kyle-n

kyle-n commented on Jan 5, 2024

@kyle-n

Why is it necessary? It's just a syntax change, you can do what's proposed with a different syntax (IIFEs).

It can be done with IIFEs, but I’ll restate my post from earlier in this thread and say IIFEs are an awkward, verbose way to code. They go against the whole point of Svelte, to write less code.

TGlide

TGlide commented on Jan 8, 2024

@TGlide
Member

It can be done with IIFEs, but I’ll restate my post from earlier in this thread and say IIFEs are an awkward, verbose way to code. They go against the whole point of Svelte, to write less code.

I'm not against the change, but saying it is necessary is a stretch. That's all I'm getting at 🙂

pothos-dev

pothos-dev commented on Jan 15, 2024

@pothos-dev

I think allowing both callback and expression syntax would be best.

In my own experience, derived signals that produce functions are very rare, so I think it's acceptable to force developers to add another indirection for this rare case:

// expression syntax for simple things
let area = $derived(width * height)

// callback syntax for long-winded things
let complicatedArea = $derived(() => {
   let taylorFactors = [1, 2, 3]
   let widthPercent = a^2 + b^2
   let width = Mat(taylorFactors).T * widthPercent
   return width * height
})

// nested callback syntax for returning functions from derived blocks
let logAreaFn = $derived(() => {
   let area = width * height
   return () => console.log(area)
})

when the compiler sees a function as the expression within $derived, it evaluates it when any states directly inside this function change. Any additional functions that are nested within the outer function would just be treated as normal closures.

There are many different signal implementations currently in the wild, so far every one that I have seen uses the callback syntax for derived signals. In not allowing this, Svelte would break with the majority of the web ecosystem, which also makes it more confusing for developers coming from other frameworks. There has to be a strong reason to that, and I don't think we have one here.

enyo

enyo commented on Jan 16, 2024

@enyo

It's very confusing for someone coming into Svelte for the first time seeing this: const countDoubled = $derived(count * 2), as this is not allowed in vanilla JS.

This is definitely allowed in vanilla JS. Nothing wrong here. You evaluate an expression and pass it to $derived, which turns it into a reactive variable.

That you don't have the overhead of thinking "how does the reactive variable get updated then" (like in react or vue) is a good thing and the power of having a preprocessor like svelte does.

17 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @enyo@brunnerh@dummdidumm@MotionlessTrain@kyle-n

        Issue actions

          Svelte 5: Introduce `$derived.with` rune · Issue #9968 · sveltejs/svelte