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.

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.