Skip to content

Conversation

@paoloricciuti
Copy link
Member

This needs discussion, but Dominic and I had this idea. Deriveds are not deeply reactive, but even nowadays you can kinda achieve deep reactivity for deriveds by doing

let local_arr = $derived.by(()=>{
	let _ = $state(arr);
	return _;
});

This is fine, but it's a bit boilerplatey. This PR is to allow a shorthand of this by allowing $state to be used in the init of a derived.

let local_arr = $derived($state(arr));

This basically gets compiled to

let local_arr = $.derived(() => $.get($.state($.proxy($$props.arr))));

So it works exactly the same.

Point of discussions:

  1. This is a solution, but it feels a bit weird...shouldn't this be a new rune instead? (I don't think we should add a new rune...just trying to think aloud)
  2. While testing this, I've discovered a quirk of this approach (which was obvious in hindsight): since when you proxify a proxy, we just return the original proxy when the prop is $state and not $state.raw you are actually mutating the parent
<script>
	import Component from './Component.svelte';
	let arr = $state([1, 2, 3]);
</script>
{arr}
<Component {arr}/>
<script>
	let { arr } = $props();

	let local_arr = $derived($state(arr));
</script>

<!-- this actually push to the parent state --!>
<button onclick={()=>{
	local_arr.push(local_arr.length + 1);
}}>push</button>

However, this is also true for the original trick that we currently "kinda" recommend.

I look at the server output, and it doesn't need any changes, since $state is just removed anyway.

WDYT should we do this?

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

@changeset-bot
Copy link

changeset-bot bot commented Dec 5, 2025

🦋 Changeset detected

Latest commit: e846ebb

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

@github-actions
Copy link
Contributor

github-actions bot commented Dec 5, 2025

Playground

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

@henrykrinkle01
Copy link
Contributor

henrykrinkle01 commented Dec 5, 2025

YES.
There must have been hundreds of Discord questions and Github issues related to this.
To avoid the parent mutation problem, currently I'm doing it like this:

let local_arr = $derived.by(()=>{
	let _ = $state($state.snapshot(arr));
	return _;
});

Which translates to

let local_arr = $derived($state($state.snapshot(arr)));

It's up to debate whether this should be the default behavior

@Conduitry
Copy link
Member

Conduitry commented Dec 5, 2025

Alternatively, we could expose a proxify function (name TBD) from the Svelte runtime, which wouldn't require any compiler changes. You would then do $derived(proxify(whatever)) when you need a mutable reactive derived instead of the $derived($state(whatever)) currently proposed.

This would also have the advantage of working in a universal load function in a +page.js (where you can't use runes), providing you with data props that you can mutate anywhere in your app and have them be reflected everywhere else.

@kran6a
Copy link

kran6a commented Dec 6, 2025

Alternatively, we could expose a proxify function (name TBD) from the Svelte runtime.

I have been doing this for months in userland by exporting functions that wrap $state and/or $state + $derived.by from .svelte.ts files. It started because I wanted to create $state on load functions, which I found extremely useful to separate app state (now resides on load functions) from UI state, event handlers, CSS and markup (in .svelte files). Having this built-in and officially supported would be awesome!

@KieranP
Copy link

KieranP commented Dec 6, 2025

This would be most helpful. After the recent change to warn state_referenced_locally on $state(prop), something like this would be very helpful to achieve a common pattern in our app: creating a new reactive state from an incoming prop.

In terms of naming/conventions, I've also seen the suggestion in some other issues for $state.from(prop), which would operate like $derived($state(prop)). It could also support the $derived.by style of supplying a function. e.g.

<script>
  const { client } = $props()

  let cats = $state.from(() => {
    client.cats.map((cat) => ({
      ...cat,
      _id: Math.random(),
    })),
  })

  function addCat() {
    cats.push({
      _id: Math.random(),
    })
  }
</script>

{#each cats as cat (cat._id)}
  <input type="text" value="{cat.name}" /><br />
{/each}

<button onclick={addCat} title="Add Cat">Add Cat</button>

In the above, if the client prop changed, any $state.from would get recalculated

@sacrosanctic
Copy link
Contributor

Pretty sure any version where $state is the leading text is wrong. $state denotes a source. Which this is not, this derives it's source.

@sillvva
Copy link

sillvva commented Dec 6, 2025

My preference would be $derived.state or $derived.proxy to clearly describe that, unlike a regular derived, this has similar properties to a deeply reactive $state proxy with dependency tracking.

But I'm also fine with $derived($state(prop))

@brunnerh
Copy link
Member

brunnerh commented Dec 6, 2025

One question with this approach would also be reassignments.

let thing = $derived($state(array));
thing = otherArray;

Is thing still reactive? I suspect not.

So having a $dervide.xxx() rune that takes care of the state wrapping internally might be better. The function approach should also work.

thing = $state(otherArray); // not allowed
thing = proxify(otherArray); // OK

@sacrosanctic
Copy link
Contributor

sacrosanctic commented Dec 6, 2025

The proxify fn seems better than overloading the terms state or derive. It's also more honest about what is happening.

function proxify(value) {
    const proxified = $state(value);
    return proxified;
}

let foo = $derived(proxify(...));

src: #16189 (comment)

@sillvva
Copy link

sillvva commented Dec 6, 2025

One question with this approach would also be reassignments.

let thing = $derived($state(array));
thing = otherArray;

Is thing still reactive? I suspect not.

If you reassign to the derived, it will break reactivity, yes. If you reassign to the prop/state it's based on it will work correctly.

https://svelte.dev/playground/1a0230ade38a4ddd8ebc64ecfa8c5d95?version=5.45.6

This foot gun would exist regardless of whether you use $derived($state(thing)) or $derived(proxify(thing)). Svelte might need another warning if you attempt to reassign to a proxified derived.

@henrykrinkle01
Copy link
Contributor

henrykrinkle01 commented Dec 6, 2025

To avoid reassignment:

	function box<T>(value: T): { current: T } {
		const boxed = $state({
			current: $state.snapshot(value)
		});
		return boxed;
	}
	const thing = $derived(box(array));
	thing.current = otherArray;

@brunnerh
Copy link
Member

brunnerh commented Dec 6, 2025

This foot gun would exist regardless of whether you use $derived($state(thing)) or $derived(proxify(thing)).

The difference is that in one case you can avoid the reactivity loss using the same syntax (proxify(thing)) and in the other you cannot because the syntax is not allowed (using $state on already declared variable).

And as noted, if the wrapping happens inside a dedicated rune, the problem would not exist at all.

@henrykrinkle01
Copy link
Contributor

I think it's possible to make $derived($state(...)) or $derived.xxx work with reassignment without using a box. That would be an improvement from the current $derived.by(...) and a justification for a new rune/syntax change. Need more compiler magic!

@sillvva
Copy link

sillvva commented Dec 6, 2025

This foot gun would exist regardless of whether you use $derived($state(thing)) or $derived(proxify(thing)).

The difference is that in one case you can avoid the reactivity loss using the same syntax (proxify(thing)) and in the other you cannot because the syntax is not allowed (using $state on already declared variable).

That still results in reactivity loss.

https://svelte.dev/playground/663f9bf9fe4e420c8b398b6b83256d16?version=5.45.6

To avoid reassignment:

	function box<T>(value: T): { current: T } {
		const boxed = $state({
			current: value
		});
		return boxed;
	}
	const thing = $derived(box(array));
	thing.current = otherArray;

That does more intuitively help you avoid the foot gun. Though the foot gun still exists if you reassign to thing. However, it's at least mitigated because you're far more likely to reassign to .current.

If not making a new rune, this is the method I'd probably go with. Otherwise, as stated, you'd probably need another warning.

@frederikhors
Copy link

Sorry if this question makes you smile (I'm the least appropriate person to answer these technical questions): why can't we have a deep reactive $derived() like $state()?

@sillvva
Copy link

sillvva commented Dec 6, 2025

My assumption is that $derived is not proxified by default, because it's less performant. Same reason $state.raw exists.

@cofl
Copy link

cofl commented Dec 6, 2025

For minimal magic-guts-exposing (exposing the behavior without lifting the nuts-and-bolts of how reactivity proxies work right to the user's face), maybe some extensions on derived would serve well?

const thing = $derived.proxy(array);
const thing = $derived.proxyOf(() => {
    // if possible
    return array
});

That would satisfy:

  1. Having a discoverable function ("hm, what's thing on $derived?")
  2. Have it be $derived (not $state)
  3. Naming it to encourage deeper learning of the system ("proxy?")
  4. Without requiring knowledge of that system to reason about at the surface level ("of course $derived($state(array)) works because $state() transforms array into a proxy with deep reactivity, and $derived ensures the toplevel state is recreated when array changes" -- did I even get that right?)

Edit: not as a replacement for what this does, but an extension.

@henrykrinkle01
Copy link
Contributor

Having a discoverable function ("hm, what's thing on $derived?")

This is the most compelling argument I've seen for a new rune. $derived($state(...)) is still kind of obscure

@KieranP
Copy link

KieranP commented Dec 6, 2025

I'm not sure proxy/proxify functions are the best approach. They expose terms that ideally should only exist within Svelte internals. And the use of $derived wrapping a $state sounds problematic because of the reassignment issue. So perhaps a new rune (or variation of existing) is the best path forward, one that returns a $state proxy, not a $derived one.

Some options being tossed around + some new ones:

  1. $state.from(prop)
  2. $state.derivedFrom(prop) (like above but more explicit as to purpose)
  3. $derived.state(prop)
  4. $derived(prop).toState() (convert from derived proxy to state proxy)

My personal preference is for options 1 or 3, but given how often I need this in our codebase, I'd be happy with any of them :-)

Another alternative would be to allow a way to make $derived deeply reactive (substitute deep for whatever):

  1. $derived.deep(prop)
  2. $derived(prop, { deep: true })
  3. $derived(prop).deeplyReactive()

@Conduitry
Copy link
Member

A significant argument against a rune in my view is the inability to use it in a SvelteKit load functions.

The name proxify does make me uncomfortable because it exposes an implementation detail. We might need to first come up with a good name for reactive objects themselves before we can name the function that produces them.

@sillvva
Copy link

sillvva commented Dec 6, 2025

A significant argument against a rune in my view is the inability to use it in a SvelteKit load functions.

This PR/discussion is about making deeply reactive $derived simpler. Either I'm misunderstanding something or I'm not sure why the inability to use it in a load function matters since you can't use $derived there either.

As for the name, Svelte doesn't really hide that it uses proxies as an implementation detail, but perhaps $derived(deep(thing)) works.

Personally I really like $derived($state(thing)). It's simple and easy to understand and doesn't expose implementation details. There's still a reassignment issue, but it's fine if you don't do that.

@sacrosanctic
Copy link
Contributor

sacrosanctic commented Dec 6, 2025

When coming up with a new API, all cases needs to be considered, not just the current one we opened an issue for. This is what Rich does and why we enjoy it. The buttery smooth vibes.

My attempt at naming:
$derived(deeply(foo))

@henrykrinkle01
Copy link
Contributor

henrykrinkle01 commented Dec 7, 2025

A significant argument against a rune in my view is the inability to use it in a SvelteKit load functions.

Why do you even need to use this feature in the load function? I think this is a client side UI thing only. I can understand when you want to proxify things in the load function but a wrapper function returning a $state should suffice.

My arguments against supporting the load function:

  1. Not sure about usage, as above.
  2. Load function is not the future, remote functions are.
  3. You can have both: a new rune and a wrapper function to make it compatible with the load function, if you really really need (I think not).
  4. More powerful tools already exist if you really want to use runes in the load function - reactive classes, in combination with transport hook.

A new rune is better than any wrapper solution because:

  1. Discoverability - as mentioned above.
  2. It can solve the reassignment problem with the help of compiler magic (similar to what's currently implemented for $state), avoiding the footgun

As for naming, I vote for either $derived.deep, $derived.proxy or $state.link (cough cough).
$derived.state is kind of confusin.

@yayza
Copy link

yayza commented Dec 7, 2025

I'm not sure proxy/proxify functions are the best approach. They expose terms that ideally should only exist within Svelte internals. And the use of $derived wrapping a $state sounds problematic because of the reassignment issue. So perhaps a new rune (or variation of existing) is the best path forward, one that returns a $state proxy, not a $derived one.

Some options being tossed around + some new ones:

  1. $state.from(prop)
  2. $state.derivedFrom(prop) (like above but more explicit as to purpose)
  3. $derived.state(prop)
  4. $derived(prop).toState() (convert from derived proxy to state proxy)

My personal preference is for options 1 or 3, but given how often I need this in our codebase, I'd be happy with any of them :-)

Another alternative would be to allow a way to make $derived deeply reactive (substitute deep for whatever):

  1. $derived.deep(prop)
  2. $derived(prop, { deep: true })
  3. $derived(prop).deeplyReactive()

+1 for $derived.deep. Pretty much explains itself and follows the same convention as writing $derived.by

@seantiz
Copy link

seantiz commented Dec 7, 2025

To avoid reassignment:

	function box<T>(value: T): { current: T } {
		const boxed = $state({
			current: $state.snapshot(value)
		});
		return boxed;
	}
	const thing = $derived(box(array));
	thing.current = otherArray;

I like this. If this doesn't get implemented, it could be an idea to run by the Runed library.

@henrykrinkle01
Copy link
Contributor

I like this. If this doesn't get implemented, it could be an idea to run by the Runed library.

Kinda already existing though. You can check out various box versions here. bits-ui uses them extensively. Personally I'm not a fan of boxes so I tend to avoid them if not absolutely necessary.
https://github.com/huntabyte/svelte-toolbelt

@KieranP
Copy link

KieranP commented Dec 9, 2025

@paoloricciuti As mentioned in some of the above comments, reactivity is lost when reassigned. I think any PR to fix this should make sure reactivity isn't lost when it is reassigned. So it might pay to add/modify a test case to your PR:

<script>
  let { arr } = $props();

  let local_arr = $derived($state(arr));

  function add() {
    local_arr.push(local_arr.length + 1);
  }

  function pop() {
    local_arr = local_arr.slice(0, -1)
  }
</script>

<button onclick={add}>Add</button>
<button onclick={pop}>Pop</button>

{#each local_arr as item}
	<p>{item}</p>
{/each}

If you click "Add", it works, and the first time you click "Pop" it will also work, but then using "Add" after that is now broken, because the "Pop" lost the $state aspect and became a shallow derived.

It's a simple example (obviously the above could just use local_arr.pop, which would not reassign), but in production code is is often more complication (e.g. an object that gets initial props on page load, and certain actions either mutate it or completely replace it). We have been trying to fix up all the new warnings in our production codebase (just shy of 79000 lines of Svelte code), but some of them can't be fixed without something like $derived.deep or some equiv.

Would be great to have some Svelte core-team members weigh in on this. I know they are all very busy on the async functions for Svelte 6. But it would be great to know if we missing something obvious that would allow us to use props in $state or an existing way to make $derived deeply reactive. If not, is there any interest in making something like $dervied.deep possible? And if not, what is the official recommendation for this use case?

@paoloricciuti
Copy link
Member Author

I mean if it is working with state just ignore the warning. Personally I would check if it is really working or if there's some underlying bug which this warning just highlighted.

If the latter is true I would try to figure out why do you need this behavior...it doesn't seem super common to me and if you have some realistic example I could maybe help you in that case. I feel like this is not a desirable behaviour most of the times (which is partially the reason deriveds are not deep in the first place)

@KieranP
Copy link

KieranP commented Dec 9, 2025

@paoloricciuti It is working, but we would prefer not to litter our codebase with ignore comments, and if we disable it globally, we do risk missing legit issues. So we'd prefer to fix the issues.

Our use case is that on page load, it fetches the current state of the records. It then passes various parts of that initial state to various child components. Those child components then create a $state from the initial props, add, change, or remove records as needed, and then use the initial props and the modified state to calculate differences to reduce network traffic.

https://svelte.dev/playground/591d1d6653a54d77ad9697413bae29a3?version=5.45.8

The playground works well, but you'll see the svelte warning in AssignmentRules.svelte. If you change that to $derived (as the warning suggests), then other things start breaking (such as the bind:value). If you change it to be the $derived.by(() => $state) approach, then after the first removeRule, it also breaks bind:value.

This pattern is something we have used 100s of times in our codebase. We need a way to create a $state object (i.e. deeply reactive) from props. But there doesn't seem to be a good way to do this right now that doesn't break after reassignment.

@paoloricciuti
Copy link
Member Author

You can do this with somewhat minimal code changes in that does involve the rest of the code those

https://svelte.dev/playground/a7d32039c63144a984eeae8fcbb0d079?version=5.45.8

However this code feels brittle, the warn is 100% right here: the moment you change the parent component to so that the props can change it will break...and tbf I'm not even sure how your diff is working...if you pass a proxied state to $state it just return the same reference so when you push you are pushing to the same array.

@KieranP
Copy link

KieranP commented Dec 9, 2025

@paoloricciuti I simplified the example, our production code is a lot more complex than this. Making the changes you suggested would require a lot of refactoring on our end.

We already had a heck of a time upgrading from Svelte 4 (we used to use $: with props, so when upgrading, the migration script changed them all to $derived, which broke everything, so we had to change them to $state manually).

A way to create a derived value that returns a deeply reactive object seems to be something a number of people need.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.