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: make deriveds writable #15570

Merged
merged 8 commits into from
Mar 21, 2025
Merged

feat: make deriveds writable #15570

merged 8 commits into from
Mar 21, 2025

Conversation

Rich-Harris
Copy link
Member

@Rich-Harris Rich-Harris commented Mar 21, 2025

See sveltejs/cli#487 (comment) for an example of why this is useful.

Docs: https://svelte.dev/docs/svelte/$derived#Overriding-derived-values

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Sorry, something went wrong.

Copy link

changeset-bot bot commented Mar 21, 2025

🦋 Changeset detected

Latest commit: b5fed42

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

@svelte-docs-bot
Copy link

Copy link
Contributor

Playground

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

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
@mdnahas
Copy link

mdnahas commented Mar 21, 2025

The issue is closed, but I want to add a note for future readers. I'm new to Svelte 5, but I think making derived writable violates user's expectations and will prevent future optimizations (both compile time and runtime). This comes from viewing derived as internal-nodes in a dataflow network.

@7nik
Copy link
Contributor

7nik commented Mar 21, 2025

You can always define the variable as const and it will be read-only. Also, derives always were and still are mutable:

let count = $state(0);
const double = $derived({ v: count*2 });

double.v = 42;

which, mentally, can not so much differ from writable derived.

@oxisto
Copy link

oxisto commented Mar 21, 2025

This solution has more issues and does not fully solve the mentioned issues, especially #14536 in combination with (sveltejs/kit#12568). Since server data is not fully reactive, this solution will not update the state of a nested property with in a data item fetched from the server, where as the ugly workaround

let { data } = $props();
let myData = $derived(data.myData);
$effect(() => {
  myData = data.myData;
});

(or similar proposed workarounds) will update also the nested properties of myData. The main use-case is fetching something from the server, have this edited by the user and send it back.

@brandonp-ais
Copy link

This solution has more issues and does not fully solve the mentioned issues, especially #14536 in combination with (sveltejs/kit#12568). Since server data is not fully reactive, this solution will not update the state of a nested property with in a data item fetched from the server, where as the ugly workaround

let { data } = $props();
let myData = $derived(data.myData);
$effect(() => {
  myData = data.myData;
});

(or similar proposed workarounds) will update also the nested properties of myData. The main use-case is fetching something from the server, have this edited by the user and send it back.

I've not tested with SvelteKit and how it works with the data prop, but it solves a common problem we have in our codebase from migration of 4 to 5.

Here is the updated REPL with commented out solution using @paoloricciuti workaround from before.

https://svelte.dev/playground/10615e463dfd4620bb97c11ae9d7394e?version=5.25.1

@hyunbinseo
Copy link
Contributor

Could the PR description be updated? I think the docs example is much more intuitive to explain why this is useful.

https://svelte.dev/docs/svelte/$derived#Overriding-derived-values

<script>
  let { post, like } = $props();

  let likes = $derived(post.likes);

  async function onclick() {
    // increment the `likes` count immediately...
    likes += 1;

    // and tell the server, which will eventually update `post`
    try {
      await like();
    } catch {
      // failed! roll back the change
      likes -= 1;
    }
  }
</script>

<button {onclick}>🧡 {likes}</button>

@Rich-Harris
Copy link
Member Author

I've linked to the docs

@MiladSedhom
Copy link

Just curious, why not something like $derived.writable()?

@paoloricciuti
Copy link
Member

Just curious, why not something like $derived.writable()?

Why add another concept when you can just use const and make js do it's work?

@mdnahas
Copy link

mdnahas commented Mar 23, 2025

Here's an alternative solution. One way to view Svelte 5 is that it is building a dataflow network, which is a directed acyclic graph ("DAG"). The $states are input nodes to the graph and $derived are internal nodes. If $derived can be written, that turns them into input nodes like $state. That would get rid of nice properties, like being written in only one location in the code, and prevent compiler optimizations in the future.

I think a better approach would be to declare some props as input nodes. That is, fully proxied values like $state(). There is already a way to declare props as "$bindable". I suggest adding a new syntax to declare them "$state".

Then the example of @hyunbinseo becomes:

<script>
  let { post, like = $state()} = $props();

  async function onclick() {
    // increment the `like` count immediately...
    like += 1;

    // and tell the server, which will eventually update `post`
    try {
      await slowServerCall();
    } catch {
      // failed! roll back the change
      like -= 1;
    }
  }
</script>

<button {onclick}>🧡 {likes}</button>

This code is clear. It doesn't need a separate likes declaration. And it preserves the $derived concept's property of being written only in its declaration.

@Rich-Harris
Copy link
Member Author

And it would be specific to props, which we're trying to avoid — it's better to have a primitive that can be used in different contexts (e.g. deeply nested properties of props rather than just the top-level props themselves) than to overload a concept like props. Under the hood your like = $state() would still be a derived (i.e. it's marked dirty then lazily re-evaluated when props.likes changes) so implementation-wise it's really not very different. I would argue that the main difference is a loss of clarity, because its derived nature is no longer explicit.

prevent compiler optimizations in the future

That's just not true! The compiler can very easily see which deriveds are written to and which aren't.

DAGs look good on a whiteboard. But in day-to-day programming it's often very useful to have an ephemeral local copy of something. Svelte has always prioritised Getting Shit Done over any kind of ideological purity, and this is a reflection of that.

@mdnahas
Copy link

mdnahas commented Mar 24, 2025

And it would be specific to props, which we're trying to avoid — it's better to have a primitive that can be used in different contexts (e.g. deeply nested properties of props rather than just the top-level props themselves) than to overload a concept like props.

I agree, there may be performance considerations if we just apply it to just top-level props.

Under the hood your like = $state() would still be a derived (i.e. it's marked dirty then lazily re-evaluated when props.likes changes) so implementation-wise it's really not very different. I would argue that the main difference is a loss of clarity, because its derived nature is no longer explicit.

That is one possible implementation of it. I don't think it is the only one.

prevent compiler optimizations in the future

That's just not true! The compiler can very easily see which deriveds are written to and which aren't.

Okay, true. But now we have 3 types of runes: state, derived, and writeable derived. When the compiler considers to apply an optimization, it needs to consider the relationships of all of those. So, the compiler got a whole lot more complicated, for a case rare enough that you didn't foresee it when you made the move to Signals months ago.

Writeable derived is not equivalent to any combination of state and derived, because the timing of writes matters. I think that is a very important consideration to think on. Writeable derived must be implemented by run-time code and cannot have compile-time implementations. And that applies to anything downstream of them, like effects.

DAGs look good on a whiteboard. But in day-to-day programming it's often very useful to have an ephemeral local copy of something. Svelte has always prioritised Getting Shit Done over any kind of ideological purity, and this is a reflection of that.

Okay. I'm an engineer at heart, but I like when math can bring clarity to a problem. And, in my experience, DAGs seem the right math here. They have a long history in compilers and in dataflow programming. And I don't think using a DAG model restrict what programmers can do. I think it would help programmers find clarity by identifying inputs (states) and keeping them separate from compute values (derived).

I understand writeable drived are handy. And I'm new here. But they seem a big change. Calculating which effects to execute must be done at runtime, because the timing of assignments now matters. Debugging will not depend on the current state of everything, but the order of assignments. If the compiler wants to exploit parallelism, it needs to have timestamps for assignments to make sure that it gets the right value out.

We've been considering the derived state that is initialized by a prop at creation and then every other write is done by 1 statement. But that's a special case for writeable derived. If a writeable derived is written in multiple places in the code, are there race conditions? Could the execution order of effects matter? As I said, writable derived seems like a big change.

If one of Svelte's advantages is a compiler, this feature makes that compiler less applicable and more complicated. I would expect that to be a priority too.

@7nik
Copy link
Contributor

7nik commented Mar 24, 2025

So, the compiler got a whole lot more complicated

Meantime, the PR diff:
in the only file related to the compiler
-30 lines
+1 line
the other 21 files are docs and tests.

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.

None yet

10 participants